scripts.js 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027
  1. class LangJinDocsUI {
  2. constructor() {
  3. this.defaults = {
  4. url: '/openapi.json'
  5. }
  6. }
  7. configure(options = {}){
  8. // 合并用户参数和默认参数
  9. this.config = { ...this.defaults, ...options };
  10. console.log("LangJinDocsUI configured with:", this.config);
  11. }
  12. async initialize(){
  13. await this.buildAPIDocsData();
  14. this.renderUI();
  15. }
  16. async buildAPIDocsData(){
  17. const response = await fetch(this.config.url);
  18. let data = await response.json();
  19. this.openapi = data.openapi;
  20. this.info = data.info;
  21. this.apiData = [];
  22. const paths = data.paths;
  23. const schemas = data.components.schemas || {};
  24. const securitySchemes = data.components.securitySchemes || {};
  25. for (let key in paths) {
  26. for (let method in paths[key]) {
  27. this.apiData.push({
  28. "operationId": paths[key][method].operationId || '',
  29. "tags": paths[key][method].tags || [],
  30. "summary": paths[key][method].summary || key,
  31. "description": paths[key][method].description || '',
  32. "path": key,
  33. "method": method.toUpperCase(),
  34. "headers": this.__parse_headers(paths[key][method].requestBody || {}),
  35. "parameters": this.__parse_parameters(paths[key][method].parameters || []),
  36. "body": this.__parse_requestBody(schemas, paths[key][method].requestBody || {}),
  37. "responses": this.__parse_responses(paths[key][method].responses || {})
  38. });
  39. }
  40. }
  41. // console.log("API Documentation Data:", JSON.stringify(this.apiData));
  42. }
  43. __parse_parameters(parameters){
  44. let params = [];
  45. for (let param of parameters) {
  46. params.push({
  47. "name": param.name,
  48. "in": param.in,
  49. "required": param.required || false,
  50. "description": param.description || '',
  51. "type": param.schema.type || 'string',
  52. "value": "" // 添加默认空值
  53. });
  54. }
  55. return params;
  56. }
  57. __parse_headers(requestBody){
  58. if (!requestBody || Object.keys(requestBody).length === 0) {
  59. return {};
  60. }
  61. const content = requestBody.content || {};
  62. const contentTypes = Object.keys(content);
  63. if (contentTypes.length === 0) {
  64. return {};
  65. }
  66. const contentType = contentTypes[0];
  67. return {"Content-Type": contentType};
  68. }
  69. __parse_requestBody(schemas, requestBody){
  70. if (!requestBody || Object.keys(requestBody).length === 0) {
  71. return {};
  72. }
  73. const content = requestBody.content || {};
  74. const contentTypes = Object.keys(content);
  75. if (contentTypes.length === 0) {
  76. return {};
  77. }
  78. const contentType = contentTypes[0];
  79. const schema = content[contentType].schema;
  80. // 检查是否有 $ref 引用
  81. if (!schema || !schema['$ref']) {
  82. return {};
  83. }
  84. const schemaRef = schema['$ref'];
  85. const key = schemaRef.replace("#/components/schemas/", "");
  86. // 检查 schemas 中是否存在该 key
  87. if (!schemas[key]) {
  88. return {};
  89. }
  90. const properties = schemas[key].properties || {};
  91. const required = schemas[key].required || [];
  92. let body = {};
  93. body[contentType] = [];
  94. for(let key in properties){
  95. body[contentType].push({
  96. "name": key,
  97. "type": properties[key].type || 'string',
  98. "description": properties[key].description || '',
  99. "required": required.includes(key),
  100. "value": ""
  101. });
  102. }
  103. return body;
  104. }
  105. __parse_responses(responses){
  106. return {};
  107. }
  108. renderUI() {
  109. const container = document.getElementById('swagger-ui');
  110. container.innerHTML = '';
  111. // 创建顶部导航栏
  112. const navBar = document.createElement('header');
  113. navBar.className = 'topbar';
  114. navBar.innerHTML = `
  115. <div class="topbar-wrapper">
  116. <a class="logo">
  117. <span>${this.info?.title || '🚀 API Documentation'}</span>
  118. </a>
  119. <div class="topbar-spacer"></div>
  120. <div class="topbar-actions">
  121. <button class="config-button" id="configButton" title="配置全局请求头">⚙️ 配置</button>
  122. </div>
  123. </div>
  124. `;
  125. container.appendChild(navBar);
  126. // 创建内容布局容器
  127. const contentContainer = document.createElement('div');
  128. contentContainer.className = 'content-container';
  129. // 创建左侧菜单
  130. const menuContainer = document.createElement('aside');
  131. menuContainer.className = 'menu-container';
  132. menuContainer.id = 'menu-container';
  133. contentContainer.appendChild(menuContainer);
  134. // 创建主内容区
  135. const mainContainer = document.createElement('main');
  136. mainContainer.className = 'main-container';
  137. mainContainer.id = 'main-container';
  138. contentContainer.appendChild(mainContainer);
  139. container.appendChild(contentContainer);
  140. // 渲染菜单项
  141. this.renderMenuItems();
  142. // 添加搜索事件监听
  143. const searchInput = document.querySelector('.search-input');
  144. searchInput.addEventListener('input', (e) => {
  145. const keyword = e.target.value.trim();
  146. this.handleSearch(keyword);
  147. });
  148. // 添加配置按钮事件监听
  149. const configButton = document.getElementById('configButton');
  150. configButton.addEventListener('click', () => {
  151. this.openConfigDialog();
  152. });
  153. }
  154. handleSearch(keyword) {
  155. const menuItems = document.querySelectorAll('.menu-item');
  156. if (keyword) {
  157. const lowerKeyword = keyword.toLowerCase();
  158. // 遍历所有标签
  159. menuItems.forEach(menuItem => {
  160. const submenu = menuItem.nextElementSibling;
  161. const apiItems = submenu.querySelectorAll('.api-item');
  162. let hasVisibleItems = false;
  163. // 遍历当前标签下的所有API
  164. apiItems.forEach(apiItem => {
  165. const apiPath = apiItem.dataset.path.toLowerCase();
  166. const apiSummary = apiItem.querySelector('.summary').textContent.toLowerCase();
  167. // 检查是否匹配搜索关键词
  168. if (apiPath.includes(lowerKeyword) || apiSummary.includes(lowerKeyword)) {
  169. apiItem.style.display = 'block';
  170. hasVisibleItems = true;
  171. } else {
  172. apiItem.style.display = 'none';
  173. }
  174. });
  175. // 如果当前标签下有可见的API,则显示该标签
  176. if (hasVisibleItems) {
  177. menuItem.style.display = 'block';
  178. // 确保搜索时展开所有包含匹配项的标签
  179. submenu.classList.add('show');
  180. menuItem.classList.add('active');
  181. menuItem.querySelector('.arrow').textContent = '▲';
  182. } else {
  183. menuItem.style.display = 'none';
  184. submenu.style.display = 'none';
  185. }
  186. });
  187. } else {
  188. // 清空搜索时恢复所有菜单项
  189. const menuItems = document.querySelectorAll('.menu-item');
  190. menuItems.forEach(menuItem => {
  191. menuItem.style.display = 'block';
  192. const submenu = menuItem.nextElementSibling;
  193. submenu.style.display = 'block';
  194. });
  195. }
  196. }
  197. renderMenuItems() {
  198. const menuContainer = document.getElementById('menu-container');
  199. // 创建搜索框
  200. const searchContainer = document.createElement('div');
  201. searchContainer.className = 'search-container';
  202. searchContainer.innerHTML = `
  203. <input class="search-input" placeholder="接口搜索..." autocomplete="off">
  204. `;
  205. menuContainer.appendChild(searchContainer);
  206. const menu = document.createElement('ul');
  207. menu.className = 'menu-list';
  208. // 按标签分组API
  209. const tagsMap = {};
  210. this.apiData.forEach(api => {
  211. api.tags.forEach(tag => {
  212. if (!tagsMap[tag]) {
  213. tagsMap[tag] = [];
  214. }
  215. tagsMap[tag].push(api);
  216. });
  217. });
  218. // 创建每个标签的菜单项
  219. const tags = Object.keys(tagsMap);
  220. for (let i = 0; i < tags.length; i++) {
  221. const tag = tags[i];
  222. const count = tagsMap[tag].length;
  223. const menuItem = document.createElement('li');
  224. menuItem.className = 'menu-item';
  225. menuItem.innerHTML = `
  226. <div class="menu-item-content">
  227. <span class="menu-item-title">${tag}</span>
  228. <div class="menu-item-actions">
  229. <span class="count">${count}</span>
  230. <span class="arrow">▼</span>
  231. </div>
  232. </div>
  233. `;
  234. menuItem.onclick = () => this.toggleMenuGroup(menuItem);
  235. menu.appendChild(menuItem);
  236. // 创建子菜单
  237. const subMenu = document.createElement('ul');
  238. subMenu.className = 'submenu';
  239. tagsMap[tag].forEach(api => {
  240. const apiItem = document.createElement('li');
  241. apiItem.className = 'api-item';
  242. apiItem.dataset.path = api.path;
  243. apiItem.dataset.method = api.method;
  244. apiItem.innerHTML = `
  245. <div class="api-item-content">
  246. <span class="method ${api.method.toLowerCase()}">${api.method}</span>
  247. <span class="summary">${api.summary}</span>
  248. </div>
  249. `;
  250. apiItem.onclick = (e) => {
  251. e.stopPropagation();
  252. this.showApiDetails(api);
  253. };
  254. subMenu.appendChild(apiItem);
  255. });
  256. menu.appendChild(subMenu);
  257. // 默认展开第一个菜单项
  258. if (i === 0) {
  259. this.toggleMenuGroup(menuItem, true);
  260. }
  261. }
  262. menuContainer.appendChild(menu);
  263. // 默认显示第一个API的详情
  264. if (this.apiData.length > 0) {
  265. this.showApiDetails(this.apiData[0]);
  266. }
  267. }
  268. toggleMenuGroup(menuItem, forceOpen = false) {
  269. const submenu = menuItem.nextElementSibling;
  270. const arrow = menuItem.querySelector('.arrow');
  271. if (submenu.classList.contains('show') && !forceOpen) {
  272. // 折叠菜单
  273. submenu.classList.remove('show');
  274. menuItem.classList.remove('active');
  275. arrow.textContent = '▼';
  276. } else {
  277. // 展开菜单
  278. submenu.classList.add('show');
  279. menuItem.classList.add('active');
  280. arrow.textContent = '▲';
  281. }
  282. }
  283. showApiDetails(api) {
  284. const mainContainer = document.getElementById('main-container');
  285. // 保存当前选中的API
  286. this.currentApi = api;
  287. // 高亮当前选中的菜单项
  288. document.querySelectorAll('.api-item').forEach(item => {
  289. item.classList.remove('active');
  290. });
  291. document.querySelector(`.api-item[data-path="${api.path}"][data-method="${api.method}"]`).classList.add('active');
  292. // 渲染API详情
  293. mainContainer.innerHTML = `
  294. <div class="api-details">
  295. <div class="api-header">
  296. <h1 class="api-title">${api.summary}</h1>
  297. <p class="api-description">${api.description || ''}</p>
  298. <div class="api-meta">
  299. <select class="method-select" id="method-select">
  300. <option value="GET" ${api.method === 'GET' ? 'selected' : ''}>GET</option>
  301. <option value="POST" ${api.method === 'POST' ? 'selected' : ''}>POST</option>
  302. <option value="PUT" ${api.method === 'PUT' ? 'selected' : ''}>PUT</option>
  303. <option value="DELETE" ${api.method === 'DELETE' ? 'selected' : ''}>DELETE</option>
  304. <option value="PATCH" ${api.method === 'PATCH' ? 'selected' : ''}>PATCH</option>
  305. </select>
  306. <input type="text" class="path-input" id="path-input" value="${api.path}">
  307. <button class="send-button" onclick="ui.sendRequest()">Send</button>
  308. </div>
  309. </div>
  310. <!-- 请求参数区域 -->
  311. <div class="section-container">
  312. <h2 class="section-title">请求参数</h2>
  313. <div class="request-tabs tabs">
  314. <button class="tab active" data-tab="parameters">Parameters</button>
  315. <button class="tab" data-tab="headers">Headers</button>
  316. <button class="tab" data-tab="requestBody">Body</button>
  317. </div>
  318. <div class="request-tab-content tab-content">
  319. <div class="tab-pane active" id="parameters">
  320. ${this.renderParametersTab(api.parameters)}
  321. </div>
  322. <div class="tab-pane" id="headers">
  323. ${this.renderHeadersTab(api.headers)}
  324. </div>
  325. <div class="tab-pane" id="requestBody">
  326. ${this.renderRequestBodyTab(api.body)}
  327. </div>
  328. </div>
  329. </div>
  330. <!-- 响应实例区域 -->
  331. <div class="section-container">
  332. <h2 class="section-title">响应实例</h2>
  333. <div class="response-section">
  334. ${this.renderResponseSection(api)}
  335. </div>
  336. </div>
  337. </div>
  338. `;
  339. // 添加标签切换事件
  340. document.querySelectorAll('.request-tabs .tab').forEach(tab => {
  341. tab.addEventListener('click', () => {
  342. document.querySelectorAll('.request-tabs .tab').forEach(t => t.classList.remove('active'));
  343. document.querySelectorAll('.request-tab-content .tab-pane').forEach(p => p.classList.remove('active'));
  344. tab.classList.add('active');
  345. document.getElementById(tab.dataset.tab).classList.add('active');
  346. });
  347. });
  348. }
  349. renderParametersTab(parameters) {
  350. let hasParams = parameters && parameters.length > 0;
  351. return `
  352. <div class="params-section">
  353. <table class="params-table">
  354. <thead>
  355. <tr>
  356. <th>参数名</th>
  357. <th>值</th>
  358. <th>类型</th>
  359. <th>是否必填</th>
  360. <th>简述</th>
  361. <th>操作</th>
  362. </tr>
  363. </thead>
  364. <tbody id="parameters-table-body">
  365. ${hasParams ? parameters.map((param, index) => `
  366. <tr>
  367. <td><input type="text" class="editable-input" value="${param.name}" placeholder="参数名"></td>
  368. <td><input type="text" class="editable-input" value="${param.value}" placeholder="值"></td>
  369. <td>
  370. <select class="editable-input">
  371. <option value="string" ${param.type === 'string' ? 'selected' : ''}>string</option>
  372. <option value="number" ${param.type === 'number' ? 'selected' : ''}>number</option>
  373. <option value="boolean" ${param.type === 'boolean' ? 'selected' : ''}>boolean</option>
  374. <option value="array" ${param.type === 'array' ? 'selected' : ''}>array</option>
  375. <option value="object" ${param.type === 'object' ? 'selected' : ''}>object</option>
  376. </select>
  377. </td>
  378. <td>
  379. <div class="checkbox-container">
  380. <input type="checkbox" ${param.required ? 'checked' : ''}>
  381. </div>
  382. </td>
  383. <td><input type="text" class="editable-input" value="${param.description}" placeholder="简述"></td>
  384. <td>
  385. <div class="action-buttons">
  386. <button class="btn btn-delete" onclick="ui.deleteParameterRow(this)">删除</button>
  387. </div>
  388. </td>
  389. </tr>
  390. `).join('') : `
  391. <tr>
  392. <td><input type="text" class="editable-input" placeholder="参数名"></td>
  393. <td><input type="text" class="editable-input" placeholder="值"></td>
  394. <td>
  395. <select class="editable-input">
  396. <option value="string">string</option>
  397. <option value="number">number</option>
  398. <option value="boolean">boolean</option>
  399. <option value="array">array</option>
  400. <option value="object">object</option>
  401. </select>
  402. </td>
  403. <td>
  404. <div class="checkbox-container">
  405. <input type="checkbox">
  406. </div>
  407. </td>
  408. <td><input type="text" class="editable-input" placeholder="简述"></td>
  409. <td>
  410. <div class="action-buttons">
  411. <button class="btn btn-delete" onclick="ui.deleteParameterRow(this)">删除</button>
  412. </div>
  413. </td>
  414. </tr>
  415. `}
  416. </tbody>
  417. </table>
  418. <div class="table-actions">
  419. <button class="btn btn-add" onclick="ui.addParameterRow()">新增参数</button>
  420. </div>
  421. </div>
  422. `;
  423. }
  424. renderHeadersTab(headers) {
  425. let hasHeaders = headers && Object.keys(headers).length > 0;
  426. return `
  427. <div class="params-section">
  428. <table class="params-table">
  429. <thead>
  430. <tr>
  431. <th>参数名</th>
  432. <th>值</th>
  433. <th>是否必填</th>
  434. <th>简述</th>
  435. <th>操作</th>
  436. </tr>
  437. </thead>
  438. <tbody id="headers-table-body">
  439. ${hasHeaders ? Object.entries(headers).map(([key, value], index) => `
  440. <tr>
  441. <td><input type="text" class="editable-input" value="${key}" placeholder="参数名"></td>
  442. <td><input type="text" class="editable-input" value="${value}" placeholder="值"></td>
  443. <td>
  444. <div class="checkbox-container">
  445. <input type="checkbox" checked>
  446. </div>
  447. </td>
  448. <td><input type="text" class="editable-input" placeholder="简述"></td>
  449. <td>
  450. <div class="action-buttons">
  451. <button class="btn btn-delete" onclick="ui.deleteHeaderRow(this)">删除</button>
  452. </div>
  453. </td>
  454. </tr>
  455. `).join('') : `
  456. <tr>
  457. <td><input type="text" class="editable-input" placeholder="参数名"></td>
  458. <td><input type="text" class="editable-input" placeholder="值"></td>
  459. <td>
  460. <div class="checkbox-container">
  461. <input type="checkbox">
  462. </div>
  463. </td>
  464. <td><input type="text" class="editable-input" placeholder="简述"></td>
  465. <td>
  466. <div class="action-buttons">
  467. <button class="btn btn-delete" onclick="ui.deleteHeaderRow(this)">删除</button>
  468. </div>
  469. </td>
  470. </tr>
  471. `}
  472. </tbody>
  473. </table>
  474. <div class="table-actions">
  475. <button class="btn btn-add" onclick="ui.addHeaderRow()">新增头部</button>
  476. </div>
  477. </div>
  478. `;
  479. }
  480. renderRequestBodyTab(body) {
  481. // 默认显示 JSON 编辑器,即使没有 body 参数
  482. const contentType = Object.keys(body)[0] || 'application/json';
  483. const params = body[contentType] || [];
  484. if (contentType.includes('application/json')) {
  485. // JSON格式的请求体
  486. const jsonValue = params.length > 0 ? JSON.stringify(this.__createRequestBodyJSON(params), null, 2) : '{}';
  487. return `
  488. <div class="json-editor-container">
  489. <textarea class="json-editor" placeholder="示例: {&quot;key&quot;: &quot;value&quot;} 请使用双引号&quot; 而不是单引号">${jsonValue}</textarea>
  490. </div>
  491. `;
  492. } else {
  493. // 表单格式的请求体
  494. return `
  495. <div class="params-section">
  496. <table class="params-table">
  497. <thead>
  498. <tr>
  499. <th>参数名</th>
  500. <th>值</th>
  501. <th>类型</th>
  502. <th>是否必填</th>
  503. <th>简述</th>
  504. <th>操作</th>
  505. </tr>
  506. </thead>
  507. <tbody id="body-table-body">
  508. ${params.length > 0 ? params.map((param, index) => `
  509. <tr>
  510. <td><input type="text" class="editable-input" value="${param.name}" placeholder="参数名"></td>
  511. <td><input type="text" class="editable-input" value="${param.value}" placeholder="值"></td>
  512. <td>
  513. <select class="editable-input">
  514. <option value="string" ${param.type === 'string' ? 'selected' : ''}>string</option>
  515. <option value="number" ${param.type === 'number' ? 'selected' : ''}>number</option>
  516. <option value="boolean" ${param.type === 'boolean' ? 'selected' : ''}>boolean</option>
  517. <option value="array" ${param.type === 'array' ? 'selected' : ''}>array</option>
  518. <option value="object" ${param.type === 'object' ? 'selected' : ''}>object</option>
  519. </select>
  520. </td>
  521. <td>
  522. <div class="checkbox-container">
  523. <input type="checkbox" ${param.required ? 'checked' : ''}>
  524. </div>
  525. </td>
  526. <td><input type="text" class="editable-input" value="${param.description}" placeholder="简述"></td>
  527. <td>
  528. <div class="action-buttons">
  529. <button class="btn btn-delete" onclick="ui.deleteBodyRow(this)">删除</button>
  530. </div>
  531. </td>
  532. </tr>
  533. `).join('') : ''}
  534. </tbody>
  535. </table>
  536. <div class="table-actions">
  537. <button class="btn btn-add" onclick="ui.addBodyRow()">新增参数</button>
  538. </div>
  539. </div>
  540. `;
  541. }
  542. }
  543. __createRequestBodyJSON(params) {
  544. const json = {};
  545. params.forEach(param => {
  546. let value = param.value;
  547. if (param.type === 'number') {
  548. value = value ? Number(value) : 0;
  549. } else if (param.type === 'boolean') {
  550. value = value.toLowerCase() === 'true' || value === '1';
  551. } else if (param.type === 'array') {
  552. value = value ? value.split(',').map(item => item.trim()) : [];
  553. } else if (param.type === 'object') {
  554. try {
  555. value = JSON.parse(value);
  556. } catch (e) {
  557. value = {};
  558. }
  559. }
  560. json[param.name] = value;
  561. });
  562. return json;
  563. }
  564. renderResponseSection(api) {
  565. return `
  566. <div class="response-container">
  567. <div class="response-body">
  568. <pre><code class="json">{}</code></pre>
  569. </div>
  570. </div>
  571. `;
  572. }
  573. sendRequest() {
  574. const method = document.getElementById('method-select').value;
  575. const path = document.getElementById('path-input').value;
  576. // 显示加载状态
  577. const responseContainer = document.querySelector('.response-body pre code');
  578. responseContainer.textContent = 'Loading...';
  579. // 获取参数
  580. const params = this.__getParametersFromForm();
  581. const headers = this.__getHeadersFromForm();
  582. const body = this.__getBodyFromForm();
  583. // 获取全局配置的请求头
  584. const globalHeaders = this.__getGlobalHeaders();
  585. // 合并请求头(全局配置优先)
  586. const mergedHeaders = { ...globalHeaders, ...headers };
  587. // 构建请求URL
  588. let url = path;
  589. if (Object.keys(params).length > 0) {
  590. url += '?' + new URLSearchParams(params).toString();
  591. }
  592. // 构建请求选项
  593. const options = {
  594. method: method,
  595. headers: mergedHeaders
  596. };
  597. // 添加请求体(如果有)
  598. if (Object.keys(body).length > 0) {
  599. options.body = JSON.stringify(body);
  600. }
  601. // 发送请求
  602. fetch(url, options)
  603. .then(response => response.json())
  604. .then(data => {
  605. // 显示响应
  606. responseContainer.textContent = JSON.stringify(data, null, 2);
  607. })
  608. .catch(error => {
  609. // 显示错误
  610. responseContainer.textContent = 'Error: ' + error.message;
  611. });
  612. }
  613. __getParametersFromForm() {
  614. const params = {};
  615. const rows = document.querySelectorAll('#parameters-table-body tr');
  616. rows.forEach(row => {
  617. const cells = row.querySelectorAll('td');
  618. const name = cells[0].querySelector('input').value;
  619. const value = cells[1].querySelector('input').value;
  620. const required = cells[3].querySelector('input').checked;
  621. if (name && (value || required)) {
  622. params[name] = value;
  623. }
  624. });
  625. return params;
  626. }
  627. __getHeadersFromForm() {
  628. const headers = {};
  629. const rows = document.querySelectorAll('#headers-table-body tr');
  630. rows.forEach(row => {
  631. const cells = row.querySelectorAll('td');
  632. const name = cells[0].querySelector('input').value;
  633. const value = cells[1].querySelector('input').value;
  634. const required = cells[2].querySelector('input').checked;
  635. if (name && (value || required)) {
  636. headers[name] = value;
  637. }
  638. });
  639. return headers;
  640. }
  641. __getBodyFromForm() {
  642. if (!this.currentApi) {
  643. return {};
  644. }
  645. const contentType = Object.keys(this.currentApi.body)[0] || 'application/json';
  646. if (contentType.includes('application/json')) {
  647. // 从JSON编辑器获取请求体
  648. const jsonEditor = document.querySelector('.json-editor');
  649. if (!jsonEditor) {
  650. return {};
  651. }
  652. let value = jsonEditor.value.trim();
  653. if (!value) {
  654. return {};
  655. }
  656. return this.robustJsonParse(value);
  657. } else {
  658. // 从表单获取请求体
  659. const body = {};
  660. const rows = document.querySelectorAll('#body-table-body tr');
  661. rows.forEach(row => {
  662. const cells = row.querySelectorAll('td');
  663. const name = cells[0].querySelector('input').value;
  664. const value = cells[1].querySelector('input').value;
  665. const required = cells[3].querySelector('input').checked;
  666. if (name && (value || required)) {
  667. body[name] = value;
  668. }
  669. });
  670. return body;
  671. }
  672. }
  673. addParameterRow() {
  674. const tableBody = document.getElementById('parameters-table-body');
  675. const newRow = document.createElement('tr');
  676. newRow.innerHTML = `
  677. <td><input type="text" class="editable-input" placeholder="参数名"></td>
  678. <td><input type="text" class="editable-input" placeholder="值"></td>
  679. <td>
  680. <select class="editable-input">
  681. <option value="string">string</option>
  682. <option value="number">number</option>
  683. <option value="boolean">boolean</option>
  684. <option value="array">array</option>
  685. <option value="object">object</option>
  686. </select>
  687. </td>
  688. <td>
  689. <div class="checkbox-container">
  690. <input type="checkbox">
  691. </div>
  692. </td>
  693. <td><input type="text" class="editable-input" placeholder="简述"></td>
  694. <td>
  695. <div class="action-buttons">
  696. <button class="btn btn-delete" onclick="ui.deleteParameterRow(this)">删除</button>
  697. </div>
  698. </td>
  699. `;
  700. tableBody.appendChild(newRow);
  701. }
  702. deleteParameterRow(button) {
  703. const row = button.closest('tr');
  704. row.remove();
  705. }
  706. addHeaderRow() {
  707. const tableBody = document.getElementById('headers-table-body');
  708. const newRow = document.createElement('tr');
  709. newRow.innerHTML = `
  710. <td><input type="text" class="editable-input" placeholder="参数名"></td>
  711. <td><input type="text" class="editable-input" placeholder="值"></td>
  712. <td>
  713. <div class="checkbox-container">
  714. <input type="checkbox">
  715. </div>
  716. </td>
  717. <td><input type="text" class="editable-input" placeholder="简述"></td>
  718. <td>
  719. <div class="action-buttons">
  720. <button class="btn btn-delete" onclick="ui.deleteHeaderRow(this)">删除</button>
  721. </div>
  722. </td>
  723. `;
  724. tableBody.appendChild(newRow);
  725. }
  726. deleteHeaderRow(button) {
  727. const row = button.closest('tr');
  728. row.remove();
  729. }
  730. addBodyRow() {
  731. const tableBody = document.getElementById('body-table-body');
  732. const newRow = document.createElement('tr');
  733. newRow.innerHTML = `
  734. <td><input type="text" class="editable-input" placeholder="参数名"></td>
  735. <td><input type="text" class="editable-input" placeholder="值"></td>
  736. <td>
  737. <select class="editable-input">
  738. <option value="string">string</option>
  739. <option value="number">number</option>
  740. <option value="boolean">boolean</option>
  741. <option value="array">array</option>
  742. <option value="object">object</option>
  743. </select>
  744. </td>
  745. <td>
  746. <div class="checkbox-container">
  747. <input type="checkbox">
  748. </div>
  749. </td>
  750. <td><input type="text" class="editable-input" placeholder="简述"></td>
  751. <td>
  752. <div class="action-buttons">
  753. <button class="btn btn-delete" onclick="ui.deleteBodyRow(this)">删除</button>
  754. </div>
  755. </td>
  756. `;
  757. tableBody.appendChild(newRow);
  758. }
  759. deleteBodyRow(button) {
  760. const row = button.closest('tr');
  761. row.remove();
  762. }
  763. // 打开配置对话框
  764. openConfigDialog() {
  765. const globalHeaders = this.__getGlobalHeaders();
  766. const configDialog = document.createElement('div');
  767. configDialog.className = 'config-dialog-overlay';
  768. configDialog.innerHTML = `
  769. <div class="config-dialog">
  770. <div class="config-dialog-header">
  771. <h3>配置全局请求头</h3>
  772. <button class="close-button" id="closeConfigDialog">×</button>
  773. </div>
  774. <div class="config-dialog-body">
  775. <table class="config-table">
  776. <thead>
  777. <tr>
  778. <th>Key</th>
  779. <th>Value</th>
  780. <th>操作</th>
  781. </tr>
  782. </thead>
  783. <tbody id="configTableBody">
  784. ${Object.entries(globalHeaders).map(([key, value]) => `
  785. <tr>
  786. <td><input type="text" class="config-input" value="${key}" placeholder="Header Key"></td>
  787. <td><input type="text" class="config-input" value="${value}" placeholder="Header Value"></td>
  788. <td><button class="btn btn-delete" onclick="ui.deleteConfigRow(this)">删除</button></td>
  789. </tr>
  790. `).join('')}
  791. </tbody>
  792. </table>
  793. <div class="table-actions">
  794. <button class="btn btn-add" onclick="ui.addConfigRow()">新增</button>
  795. </div>
  796. </div>
  797. <div class="config-dialog-footer">
  798. <button class="btn btn-primary" onclick="ui.saveConfig()">保存</button>
  799. <button class="btn" onclick="ui.closeConfigDialog()">取消</button>
  800. </div>
  801. </div>
  802. `;
  803. document.body.appendChild(configDialog);
  804. // 绑定关闭事件
  805. document.getElementById('closeConfigDialog').addEventListener('click', () => this.closeConfigDialog());
  806. configDialog.addEventListener('click', (e) => {
  807. if (e.target === configDialog) {
  808. this.closeConfigDialog();
  809. }
  810. });
  811. }
  812. // 关闭配置对话框
  813. closeConfigDialog() {
  814. const dialog = document.querySelector('.config-dialog-overlay');
  815. if (dialog) {
  816. dialog.remove();
  817. }
  818. }
  819. // 添加配置行
  820. addConfigRow() {
  821. const tableBody = document.getElementById('configTableBody');
  822. const newRow = document.createElement('tr');
  823. newRow.innerHTML = `
  824. <td><input type="text" class="config-input" placeholder="Header Key"></td>
  825. <td><input type="text" class="config-input" placeholder="Header Value"></td>
  826. <td><button class="btn btn-delete" onclick="ui.deleteConfigRow(this)">删除</button></td>
  827. `;
  828. tableBody.appendChild(newRow);
  829. }
  830. // 删除配置行
  831. deleteConfigRow(button) {
  832. const row = button.closest('tr');
  833. row.remove();
  834. }
  835. // 保存配置
  836. saveConfig() {
  837. const rows = document.querySelectorAll('#configTableBody tr');
  838. const headers = {};
  839. rows.forEach(row => {
  840. const cells = row.querySelectorAll('td');
  841. const key = cells[0].querySelector('input').value.trim();
  842. const value = cells[1].querySelector('input').value.trim();
  843. if (key && value) {
  844. headers[key] = value;
  845. }
  846. });
  847. // 保存到本地存储
  848. localStorage.setItem('globalHeaders', JSON.stringify(headers));
  849. // 显示成功提示
  850. this.showNotification('配置已保存');
  851. // 关闭对话框
  852. this.closeConfigDialog();
  853. }
  854. // 从本地存储获取全局请求头
  855. __getGlobalHeaders() {
  856. const stored = localStorage.getItem('globalHeaders');
  857. if (stored) {
  858. try {
  859. return JSON.parse(stored);
  860. } catch (e) {
  861. return {};
  862. }
  863. }
  864. return {};
  865. }
  866. // 显示通知
  867. showNotification(message) {
  868. const notification = document.createElement('div');
  869. notification.className = 'notification';
  870. notification.textContent = message;
  871. document.body.appendChild(notification);
  872. setTimeout(() => {
  873. notification.classList.add('show');
  874. }, 10);
  875. setTimeout(() => {
  876. notification.classList.remove('show');
  877. setTimeout(() => notification.remove(), 300);
  878. }, 2000);
  879. }
  880. // 增强版JSON解析,处理非标准JSON格式
  881. robustJsonParse(str) {
  882. if (!str || !str.trim()) return {};
  883. const trimmed = str.trim();
  884. try {
  885. return JSON.parse(trimmed);
  886. } catch (e) {
  887. console.log('标准JSON解析失败,尝试修复:', trimmed);
  888. try {
  889. let processed = trimmed;
  890. // 1. 处理用单引号包裹整个对象的情况 (您原有的逻辑,很好)
  891. if (processed.startsWith("'") && processed.endsWith("'")) {
  892. processed = processed.slice(1, -1);
  893. }
  894. // 2. 【核心改进】智能替换键名
  895. // 匹配 'key': 或 "key": 或 key: 的模式,统一替换为 "key":
  896. // (\w+) 捕获键名 (字母、数字、下划线)
  897. processed = processed.replace(/['"]?(\w+)['"]?\s*:/g, '"$1":');
  898. // 3. 【核心改进】智能替换字符串值
  899. // 匹配 : '...' 的模式,只替换值的单引号为双引号
  900. // 这样可以确保不会影响到字符串内部的合法单引号
  901. // ([^']*) 捕获不包含单引号的任意字符序列
  902. processed = processed.replace(/:\s*'([^']*)'/g, ':"$1"');
  903. // 4. (可选) 处理布尔值和null的小写问题 (如果后端返回的是大写 TRUE/FALSE)
  904. // processed = processed.replace(/\bTRUE\b/g, 'true');
  905. // processed = processed.replace(/\bFALSE\b/g, 'false');
  906. // processed = processed.replace(/\bNULL\b/g, 'null');
  907. // 5. (可选) 清理可能因替换产生的多余逗号,例如 [1,2,,] -> [1,2]
  908. // processed = processed.replace(/,\s*,/g, ',').replace(/,\s*]/g, ']').replace(/,\s*}/g, '}');
  909. console.log('修复后的JSON字符串:', processed);
  910. // 6. 尝试解析修复后的字符串
  911. const result = JSON.parse(processed);
  912. console.log('修复后的JSON对象:', result);
  913. return result;
  914. } catch (e2) {
  915. // 【您原有的错误处理逻辑,保持不变】
  916. console.error('JSON解析最终失败:', e2);
  917. console.error('失败的处理结果:', processed);
  918. this.showNotification('JSON 格式错误,请检查引号和语法');
  919. return {};
  920. }
  921. }
  922. }
  923. }
  924. const ui = new LangJinDocsUI();