class LangJinDocsUI { constructor() { this.defaults = { url: '/openapi.json' } } configure(options = {}){ // 合并用户参数和默认参数 this.config = { ...this.defaults, ...options }; console.log("LangJinDocsUI configured with:", this.config); } async initialize(){ await this.buildAPIDocsData(); this.renderUI(); } async buildAPIDocsData(){ const response = await fetch(this.config.url); let data = await response.json(); this.openapi = data.openapi; this.info = data.info; this.apiData = []; const paths = data.paths; const schemas = data.components.schemas || {}; const securitySchemes = data.components.securitySchemes || {}; for (let key in paths) { for (let method in paths[key]) { this.apiData.push({ "operationId": paths[key][method].operationId || '', "tags": paths[key][method].tags || [], "summary": paths[key][method].summary || key, "description": paths[key][method].description || '', "path": key, "method": method.toUpperCase(), "headers": this.__parse_headers(paths[key][method].requestBody || {}), "parameters": this.__parse_parameters(paths[key][method].parameters || []), "body": this.__parse_requestBody(schemas, paths[key][method].requestBody || {}), "responses": this.__parse_responses(paths[key][method].responses || {}) }); } } // console.log("API Documentation Data:", JSON.stringify(this.apiData)); } __parse_parameters(parameters){ let params = []; for (let param of parameters) { params.push({ "name": param.name, "in": param.in, "required": param.required || false, "description": param.description || '', "type": param.schema.type || 'string', "value": "" // 添加默认空值 }); } return params; } __parse_headers(requestBody){ if (!requestBody || Object.keys(requestBody).length === 0) { return {}; } const content = requestBody.content || {}; const contentTypes = Object.keys(content); if (contentTypes.length === 0) { return {}; } const contentType = contentTypes[0]; return {"Content-Type": contentType}; } __parse_requestBody(schemas, requestBody){ if (!requestBody || Object.keys(requestBody).length === 0) { return {}; } const content = requestBody.content || {}; const contentTypes = Object.keys(content); if (contentTypes.length === 0) { return {}; } const contentType = contentTypes[0]; const schema = content[contentType].schema; // 检查是否有 $ref 引用 if (!schema || !schema['$ref']) { return {}; } const schemaRef = schema['$ref']; const key = schemaRef.replace("#/components/schemas/", ""); // 检查 schemas 中是否存在该 key if (!schemas[key]) { return {}; } const properties = schemas[key].properties || {}; const required = schemas[key].required || []; let body = {}; body[contentType] = []; for(let key in properties){ body[contentType].push({ "name": key, "type": properties[key].type || 'string', "description": properties[key].description || '', "required": required.includes(key), "value": "" }); } return body; } __parse_responses(responses){ return {}; } renderUI() { const container = document.getElementById('swagger-ui'); container.innerHTML = ''; // 创建顶部导航栏 const navBar = document.createElement('header'); navBar.className = 'topbar'; navBar.innerHTML = `
`; container.appendChild(navBar); // 创建内容布局容器 const contentContainer = document.createElement('div'); contentContainer.className = 'content-container'; // 创建左侧菜单 const menuContainer = document.createElement('aside'); menuContainer.className = 'menu-container'; menuContainer.id = 'menu-container'; contentContainer.appendChild(menuContainer); // 创建主内容区 const mainContainer = document.createElement('main'); mainContainer.className = 'main-container'; mainContainer.id = 'main-container'; contentContainer.appendChild(mainContainer); container.appendChild(contentContainer); // 渲染菜单项 this.renderMenuItems(); // 添加搜索事件监听 const searchInput = document.querySelector('.search-input'); searchInput.addEventListener('input', (e) => { const keyword = e.target.value.trim(); this.handleSearch(keyword); }); // 添加配置按钮事件监听 const configButton = document.getElementById('configButton'); configButton.addEventListener('click', () => { this.openConfigDialog(); }); } handleSearch(keyword) { const menuItems = document.querySelectorAll('.menu-item'); if (keyword) { const lowerKeyword = keyword.toLowerCase(); // 遍历所有标签 menuItems.forEach(menuItem => { const submenu = menuItem.nextElementSibling; const apiItems = submenu.querySelectorAll('.api-item'); let hasVisibleItems = false; // 遍历当前标签下的所有API apiItems.forEach(apiItem => { const apiPath = apiItem.dataset.path.toLowerCase(); const apiSummary = apiItem.querySelector('.summary').textContent.toLowerCase(); // 检查是否匹配搜索关键词 if (apiPath.includes(lowerKeyword) || apiSummary.includes(lowerKeyword)) { apiItem.style.display = 'block'; hasVisibleItems = true; } else { apiItem.style.display = 'none'; } }); // 如果当前标签下有可见的API,则显示该标签 if (hasVisibleItems) { menuItem.style.display = 'block'; // 确保搜索时展开所有包含匹配项的标签 submenu.classList.add('show'); menuItem.classList.add('active'); menuItem.querySelector('.arrow').textContent = '▲'; } else { menuItem.style.display = 'none'; submenu.style.display = 'none'; } }); } else { // 清空搜索时恢复所有菜单项 const menuItems = document.querySelectorAll('.menu-item'); menuItems.forEach(menuItem => { menuItem.style.display = 'block'; const submenu = menuItem.nextElementSibling; submenu.style.display = 'block'; }); } } renderMenuItems() { const menuContainer = document.getElementById('menu-container'); // 创建搜索框 const searchContainer = document.createElement('div'); searchContainer.className = 'search-container'; searchContainer.innerHTML = ` `; menuContainer.appendChild(searchContainer); const menu = document.createElement('ul'); menu.className = 'menu-list'; // 按标签分组API const tagsMap = {}; this.apiData.forEach(api => { api.tags.forEach(tag => { if (!tagsMap[tag]) { tagsMap[tag] = []; } tagsMap[tag].push(api); }); }); // 创建每个标签的菜单项 const tags = Object.keys(tagsMap); for (let i = 0; i < tags.length; i++) { const tag = tags[i]; const count = tagsMap[tag].length; const menuItem = document.createElement('li'); menuItem.className = 'menu-item'; menuItem.innerHTML = ` `; menuItem.onclick = () => this.toggleMenuGroup(menuItem); menu.appendChild(menuItem); // 创建子菜单 const subMenu = document.createElement('ul'); subMenu.className = 'submenu'; tagsMap[tag].forEach(api => { const apiItem = document.createElement('li'); apiItem.className = 'api-item'; apiItem.dataset.path = api.path; apiItem.dataset.method = api.method; apiItem.innerHTML = `
${api.method} ${api.summary}
`; apiItem.onclick = (e) => { e.stopPropagation(); this.showApiDetails(api); }; subMenu.appendChild(apiItem); }); menu.appendChild(subMenu); // 默认展开第一个菜单项 if (i === 0) { this.toggleMenuGroup(menuItem, true); } } menuContainer.appendChild(menu); // 默认显示第一个API的详情 if (this.apiData.length > 0) { this.showApiDetails(this.apiData[0]); } } toggleMenuGroup(menuItem, forceOpen = false) { const submenu = menuItem.nextElementSibling; const arrow = menuItem.querySelector('.arrow'); if (submenu.classList.contains('show') && !forceOpen) { // 折叠菜单 submenu.classList.remove('show'); menuItem.classList.remove('active'); arrow.textContent = '▼'; } else { // 展开菜单 submenu.classList.add('show'); menuItem.classList.add('active'); arrow.textContent = '▲'; } } showApiDetails(api) { const mainContainer = document.getElementById('main-container'); // 保存当前选中的API this.currentApi = api; // 高亮当前选中的菜单项 document.querySelectorAll('.api-item').forEach(item => { item.classList.remove('active'); }); document.querySelector(`.api-item[data-path="${api.path}"][data-method="${api.method}"]`).classList.add('active'); // 渲染API详情 mainContainer.innerHTML = `

${api.summary}

${api.description || ''}

请求参数

${this.renderParametersTab(api.parameters)}
${this.renderHeadersTab(api.headers)}
${this.renderRequestBodyTab(api.body)}

响应实例

${this.renderResponseSection(api)}
`; // 添加标签切换事件 document.querySelectorAll('.request-tabs .tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.request-tabs .tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.request-tab-content .tab-pane').forEach(p => p.classList.remove('active')); tab.classList.add('active'); document.getElementById(tab.dataset.tab).classList.add('active'); }); }); } renderParametersTab(parameters) { let hasParams = parameters && parameters.length > 0; return `
${hasParams ? parameters.map((param, index) => ` `).join('') : ` `}
参数名 类型 是否必填 简述 操作
`; } renderHeadersTab(headers) { let hasHeaders = headers && Object.keys(headers).length > 0; return `
${hasHeaders ? Object.entries(headers).map(([key, value], index) => ` `).join('') : ` `}
参数名 是否必填 简述 操作
`; } renderRequestBodyTab(body) { // 默认显示 JSON 编辑器,即使没有 body 参数 const contentType = Object.keys(body)[0] || 'application/json'; const params = body[contentType] || []; if (contentType.includes('application/json')) { // JSON格式的请求体 const jsonValue = params.length > 0 ? JSON.stringify(this.__createRequestBodyJSON(params), null, 2) : '{}'; return `
`; } else { // 表单格式的请求体 return `
${params.length > 0 ? params.map((param, index) => ` `).join('') : ''}
参数名 类型 是否必填 简述 操作
`; } } __createRequestBodyJSON(params) { const json = {}; params.forEach(param => { let value = param.value; if (param.type === 'number') { value = value ? Number(value) : 0; } else if (param.type === 'boolean') { value = value.toLowerCase() === 'true' || value === '1'; } else if (param.type === 'array') { value = value ? value.split(',').map(item => item.trim()) : []; } else if (param.type === 'object') { try { value = JSON.parse(value); } catch (e) { value = {}; } } json[param.name] = value; }); return json; } renderResponseSection(api) { return `
{}
`; } sendRequest() { const method = document.getElementById('method-select').value; const path = document.getElementById('path-input').value; // 显示加载状态 const responseContainer = document.querySelector('.response-body pre code'); responseContainer.textContent = 'Loading...'; // 获取参数 const params = this.__getParametersFromForm(); const headers = this.__getHeadersFromForm(); const body = this.__getBodyFromForm(); // 获取全局配置的请求头 const globalHeaders = this.__getGlobalHeaders(); // 合并请求头(全局配置优先) const mergedHeaders = { ...globalHeaders, ...headers }; // 构建请求URL let url = path; if (Object.keys(params).length > 0) { url += '?' + new URLSearchParams(params).toString(); } // 构建请求选项 const options = { method: method, headers: mergedHeaders }; // 添加请求体(如果有) if (Object.keys(body).length > 0) { options.body = JSON.stringify(body); } // 发送请求 fetch(url, options) .then(response => response.json()) .then(data => { // 显示响应 responseContainer.textContent = JSON.stringify(data, null, 2); }) .catch(error => { // 显示错误 responseContainer.textContent = 'Error: ' + error.message; }); } __getParametersFromForm() { const params = {}; const rows = document.querySelectorAll('#parameters-table-body tr'); rows.forEach(row => { const cells = row.querySelectorAll('td'); const name = cells[0].querySelector('input').value; const value = cells[1].querySelector('input').value; const required = cells[3].querySelector('input').checked; if (name && (value || required)) { params[name] = value; } }); return params; } __getHeadersFromForm() { const headers = {}; const rows = document.querySelectorAll('#headers-table-body tr'); rows.forEach(row => { const cells = row.querySelectorAll('td'); const name = cells[0].querySelector('input').value; const value = cells[1].querySelector('input').value; const required = cells[2].querySelector('input').checked; if (name && (value || required)) { headers[name] = value; } }); return headers; } __getBodyFromForm() { if (!this.currentApi) { return {}; } const contentType = Object.keys(this.currentApi.body)[0] || 'application/json'; if (contentType.includes('application/json')) { // 从JSON编辑器获取请求体 const jsonEditor = document.querySelector('.json-editor'); if (!jsonEditor) { return {}; } let value = jsonEditor.value.trim(); if (!value) { return {}; } return this.robustJsonParse(value); } else { // 从表单获取请求体 const body = {}; const rows = document.querySelectorAll('#body-table-body tr'); rows.forEach(row => { const cells = row.querySelectorAll('td'); const name = cells[0].querySelector('input').value; const value = cells[1].querySelector('input').value; const required = cells[3].querySelector('input').checked; if (name && (value || required)) { body[name] = value; } }); return body; } } addParameterRow() { const tableBody = document.getElementById('parameters-table-body'); const newRow = document.createElement('tr'); newRow.innerHTML = `
`; tableBody.appendChild(newRow); } deleteParameterRow(button) { const row = button.closest('tr'); row.remove(); } addHeaderRow() { const tableBody = document.getElementById('headers-table-body'); const newRow = document.createElement('tr'); newRow.innerHTML = `
`; tableBody.appendChild(newRow); } deleteHeaderRow(button) { const row = button.closest('tr'); row.remove(); } addBodyRow() { const tableBody = document.getElementById('body-table-body'); const newRow = document.createElement('tr'); newRow.innerHTML = `
`; tableBody.appendChild(newRow); } deleteBodyRow(button) { const row = button.closest('tr'); row.remove(); } // 打开配置对话框 openConfigDialog() { const globalHeaders = this.__getGlobalHeaders(); const configDialog = document.createElement('div'); configDialog.className = 'config-dialog-overlay'; configDialog.innerHTML = `

配置全局请求头

${Object.entries(globalHeaders).map(([key, value]) => ` `).join('')}
Key Value 操作
`; document.body.appendChild(configDialog); // 绑定关闭事件 document.getElementById('closeConfigDialog').addEventListener('click', () => this.closeConfigDialog()); configDialog.addEventListener('click', (e) => { if (e.target === configDialog) { this.closeConfigDialog(); } }); } // 关闭配置对话框 closeConfigDialog() { const dialog = document.querySelector('.config-dialog-overlay'); if (dialog) { dialog.remove(); } } // 添加配置行 addConfigRow() { const tableBody = document.getElementById('configTableBody'); const newRow = document.createElement('tr'); newRow.innerHTML = ` `; tableBody.appendChild(newRow); } // 删除配置行 deleteConfigRow(button) { const row = button.closest('tr'); row.remove(); } // 保存配置 saveConfig() { const rows = document.querySelectorAll('#configTableBody tr'); const headers = {}; rows.forEach(row => { const cells = row.querySelectorAll('td'); const key = cells[0].querySelector('input').value.trim(); const value = cells[1].querySelector('input').value.trim(); if (key && value) { headers[key] = value; } }); // 保存到本地存储 localStorage.setItem('globalHeaders', JSON.stringify(headers)); // 显示成功提示 this.showNotification('配置已保存'); // 关闭对话框 this.closeConfigDialog(); } // 从本地存储获取全局请求头 __getGlobalHeaders() { const stored = localStorage.getItem('globalHeaders'); if (stored) { try { return JSON.parse(stored); } catch (e) { return {}; } } return {}; } // 显示通知 showNotification(message) { const notification = document.createElement('div'); notification.className = 'notification'; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.classList.add('show'); }, 10); setTimeout(() => { notification.classList.remove('show'); setTimeout(() => notification.remove(), 300); }, 2000); } // 增强版JSON解析,处理非标准JSON格式 robustJsonParse(str) { if (!str || !str.trim()) return {}; const trimmed = str.trim(); try { return JSON.parse(trimmed); } catch (e) { console.log('标准JSON解析失败,尝试修复:', trimmed); try { let processed = trimmed; // 1. 处理用单引号包裹整个对象的情况 (您原有的逻辑,很好) if (processed.startsWith("'") && processed.endsWith("'")) { processed = processed.slice(1, -1); } // 2. 【核心改进】智能替换键名 // 匹配 'key': 或 "key": 或 key: 的模式,统一替换为 "key": // (\w+) 捕获键名 (字母、数字、下划线) processed = processed.replace(/['"]?(\w+)['"]?\s*:/g, '"$1":'); // 3. 【核心改进】智能替换字符串值 // 匹配 : '...' 的模式,只替换值的单引号为双引号 // 这样可以确保不会影响到字符串内部的合法单引号 // ([^']*) 捕获不包含单引号的任意字符序列 processed = processed.replace(/:\s*'([^']*)'/g, ':"$1"'); // 4. (可选) 处理布尔值和null的小写问题 (如果后端返回的是大写 TRUE/FALSE) // processed = processed.replace(/\bTRUE\b/g, 'true'); // processed = processed.replace(/\bFALSE\b/g, 'false'); // processed = processed.replace(/\bNULL\b/g, 'null'); // 5. (可选) 清理可能因替换产生的多余逗号,例如 [1,2,,] -> [1,2] // processed = processed.replace(/,\s*,/g, ',').replace(/,\s*]/g, ']').replace(/,\s*}/g, '}'); console.log('修复后的JSON字符串:', processed); // 6. 尝试解析修复后的字符串 const result = JSON.parse(processed); console.log('修复后的JSON对象:', result); return result; } catch (e2) { // 【您原有的错误处理逻辑,保持不变】 console.error('JSON解析最终失败:', e2); console.error('失败的处理结果:', processed); this.showNotification('JSON 格式错误,请检查引号和语法'); return {}; } } } } const ui = new LangJinDocsUI();