reader.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. let currentCategory = '';
  2. let currentDoc = '';
  3. let docList = [];
  4. // DOM 元素缓存
  5. const DOM = {
  6. loading: null,
  7. content: null,
  8. docNav: null,
  9. toc: null,
  10. leftSidebar: null,
  11. rightSidebar: null
  12. };
  13. // 配置 marked
  14. marked.setOptions({
  15. highlight: function(code, lang) {
  16. if (lang && hljs.getLanguage(lang)) {
  17. try {
  18. return hljs.highlight(code, { language: lang }).value;
  19. } catch (err) {
  20. console.error('Highlight error:', err);
  21. }
  22. }
  23. return hljs.highlightAuto(code).value;
  24. },
  25. breaks: true,
  26. gfm: true
  27. });
  28. // 从 URL 获取参数
  29. function getQueryParams() {
  30. const params = new URLSearchParams(window.location.search);
  31. return {
  32. category: params.get('category'),
  33. doc: params.get('doc')
  34. };
  35. }
  36. // 加载文档列表
  37. async function loadDocList() {
  38. const { category } = getQueryParams();
  39. if (!category) {
  40. window.location.href = '/';
  41. return;
  42. }
  43. currentCategory = category;
  44. const categoryNameSpan = document.querySelector('#category-title .category-name');
  45. if (categoryNameSpan) {
  46. categoryNameSpan.textContent = category;
  47. }
  48. try {
  49. const response = await fetch(`/api/category/${encodeURIComponent(category)}`);
  50. if (!response.ok) throw new Error('获取文档列表失败');
  51. const categoryData = await response.json();
  52. docList = categoryData.docs;
  53. renderDocNav(docList);
  54. // 加载第一篇文档(或 URL 指定的文档)
  55. const { doc } = getQueryParams();
  56. const firstDoc = doc || docList[0]?.name;
  57. if (firstDoc) {
  58. loadDocument(firstDoc);
  59. }
  60. } catch (err) {
  61. console.error('Error loading doc list:', err);
  62. showError('加载文档列表失败');
  63. }
  64. }
  65. // 渲染文档导航
  66. function renderDocNav(docs) {
  67. const nav = DOM.docNav || document.getElementById('doc-nav');
  68. if (!DOM.docNav) DOM.docNav = nav;
  69. nav.innerHTML = '';
  70. docs.forEach(doc => {
  71. const item = document.createElement('div');
  72. item.className = `nav-item level-${doc.level}`;
  73. item.dataset.docName = doc.name;
  74. const link = document.createElement('a');
  75. link.href = '#';
  76. link.className = 'nav-link';
  77. link.textContent = doc.name;
  78. item.appendChild(link);
  79. nav.appendChild(item);
  80. });
  81. // 使用事件委托 - 只添加一个监听器
  82. nav.removeEventListener('click', handleNavClick);
  83. nav.addEventListener('click', handleNavClick);
  84. }
  85. // 导航点击处理函数
  86. function handleNavClick(e) {
  87. if (e.target.classList.contains('nav-link')) {
  88. e.preventDefault();
  89. const item = e.target.parentElement;
  90. const docName = item.dataset.docName;
  91. if (docName) {
  92. loadDocument(docName);
  93. updateURL(currentCategory, docName);
  94. // 更新活动状态 - 使用缓存的查询结果
  95. const navItems = DOM.docNav.querySelectorAll('.nav-item');
  96. navItems.forEach(el => el.classList.toggle('active', el === item));
  97. }
  98. }
  99. }
  100. // 加载文档内容
  101. async function loadDocument(docName, scrollToText = null) {
  102. // 使用 DOM 缓存
  103. if (!DOM.loading) DOM.loading = document.getElementById('loading');
  104. if (!DOM.content) DOM.content = document.getElementById('markdown-content');
  105. DOM.loading.style.display = 'flex';
  106. DOM.content.innerHTML = '';
  107. try {
  108. const response = await fetch(`/api/doc/${encodeURIComponent(currentCategory)}/${encodeURIComponent(docName)}`);
  109. if (!response.ok) {
  110. const errorData = await response.json().catch(() => ({}));
  111. throw new Error(errorData.error || `加载失败 (${response.status})`);
  112. }
  113. const data = await response.json();
  114. // 验证数据有效性
  115. if (!data.content || typeof data.content !== 'string') {
  116. throw new Error('文档内容格式错误');
  117. }
  118. currentDoc = docName;
  119. // 渲染 Markdown
  120. const html = marked.parse(data.content);
  121. DOM.content.innerHTML = html;
  122. // 生成 TOC
  123. generateTOC();
  124. // 更新活动文档 - 使用 toggle 优化
  125. if (DOM.docNav) {
  126. const navItems = DOM.docNav.querySelectorAll('.nav-item');
  127. navItems.forEach(el => el.classList.toggle('active', el.dataset.docName === docName));
  128. }
  129. DOM.loading.style.display = 'none';
  130. // 如果指定了要滚动到的文本,等待渲染完成后滚动
  131. if (scrollToText) {
  132. setTimeout(() => {
  133. scrollToSearchMatch(scrollToText);
  134. }, 100);
  135. }
  136. } catch (err) {
  137. console.error('Error loading document:', err);
  138. DOM.loading.style.display = 'none';
  139. showError(`加载文档失败:${err.message || '请稍后重试'}`);
  140. }
  141. }
  142. // 生成 TOC
  143. function generateTOC() {
  144. // 使用 DOM 缓存
  145. if (!DOM.toc) DOM.toc = document.getElementById('toc');
  146. const headings = DOM.content.querySelectorAll('h1, h2, h3, h4, h5, h6');
  147. DOM.toc.innerHTML = '';
  148. if (headings.length === 0) {
  149. DOM.toc.innerHTML = '<p class="toc-empty">本文档没有标题</p>';
  150. return;
  151. }
  152. // 创建标题 ID 映射
  153. const headingMap = new Map();
  154. headings.forEach((heading, index) => {
  155. // 给标题添加 ID
  156. if (!heading.id) {
  157. heading.id = `heading-${index}`;
  158. }
  159. const level = parseInt(heading.tagName.substring(1));
  160. const link = document.createElement('a');
  161. link.href = `#${heading.id}`;
  162. link.className = `toc-link toc-level-${level}`;
  163. link.textContent = heading.textContent;
  164. headingMap.set(heading.id, heading);
  165. DOM.toc.appendChild(link);
  166. });
  167. // 使用事件委托 - 只添加一个监听器
  168. DOM.toc.removeEventListener('click', handleTocClick);
  169. DOM.toc.addEventListener('click', (e) => handleTocClick(e, headingMap));
  170. // 监听滚动,高亮当前标题
  171. setupScrollSpy(headings);
  172. }
  173. // TOC 点击处理函数
  174. function handleTocClick(e, headingMap) {
  175. if (e.target.classList.contains('toc-link')) {
  176. e.preventDefault();
  177. const headingId = e.target.getAttribute('href').substring(1);
  178. const heading = headingMap.get(headingId);
  179. if (heading) {
  180. heading.scrollIntoView({ behavior: 'smooth', block: 'start' });
  181. // 更新活动状态
  182. const tocLinks = DOM.toc.querySelectorAll('.toc-link');
  183. tocLinks.forEach(el => el.classList.toggle('active', el === e.target));
  184. }
  185. }
  186. }
  187. // 滚动监听
  188. function setupScrollSpy(headings) {
  189. let activeLink = null;
  190. const observer = new IntersectionObserver((entries) => {
  191. entries.forEach(entry => {
  192. if (entry.isIntersecting) {
  193. const id = entry.target.id;
  194. const link = DOM.toc.querySelector(`.toc-link[href="#${id}"]`);
  195. if (link && link !== activeLink) {
  196. // 只在需要时更新,减少 DOM 操作
  197. if (activeLink) activeLink.classList.remove('active');
  198. link.classList.add('active');
  199. activeLink = link;
  200. }
  201. }
  202. });
  203. }, {
  204. rootMargin: '-100px 0px -80% 0px',
  205. threshold: 0
  206. });
  207. headings.forEach(heading => observer.observe(heading));
  208. }
  209. // 更新 URL
  210. function updateURL(category, doc) {
  211. const url = new URL(window.location);
  212. url.searchParams.set('category', category);
  213. url.searchParams.set('doc', doc);
  214. window.history.pushState({}, '', url);
  215. }
  216. // 显示错误
  217. function showError(message) {
  218. const content = document.getElementById('markdown-content');
  219. content.innerHTML = `<div class="error-message">${message}</div>`;
  220. }
  221. // 切换侧边栏(移动端)
  222. function setupSidebarToggles() {
  223. const toggleLeft = document.getElementById('toggle-left');
  224. const toggleRight = document.getElementById('toggle-right');
  225. // 使用 DOM 缓存
  226. if (!DOM.leftSidebar) DOM.leftSidebar = document.getElementById('left-sidebar');
  227. if (!DOM.rightSidebar) DOM.rightSidebar = document.getElementById('right-sidebar');
  228. // 移动端/平板初始化:默认折叠侧边栏
  229. if (window.innerWidth <= 768) {
  230. DOM.leftSidebar.classList.add('collapsed');
  231. DOM.rightSidebar.classList.add('collapsed');
  232. } else if (window.innerWidth <= 1024) {
  233. // 平板:只折叠右侧栏
  234. DOM.rightSidebar.classList.add('collapsed');
  235. }
  236. // 监听窗口大小变化
  237. window.addEventListener('resize', () => {
  238. if (window.innerWidth <= 768) {
  239. // 移动端:确保左右侧边栏都折叠
  240. if (!DOM.leftSidebar.classList.contains('collapsed')) {
  241. DOM.leftSidebar.classList.add('collapsed');
  242. }
  243. if (!DOM.rightSidebar.classList.contains('collapsed')) {
  244. DOM.rightSidebar.classList.add('collapsed');
  245. }
  246. } else if (window.innerWidth <= 1024) {
  247. // 平板:只折叠右侧栏,展开左侧栏
  248. DOM.leftSidebar.classList.remove('collapsed');
  249. if (!DOM.rightSidebar.classList.contains('collapsed')) {
  250. DOM.rightSidebar.classList.add('collapsed');
  251. }
  252. } else {
  253. // 桌面端:展开所有侧边栏
  254. DOM.leftSidebar.classList.remove('collapsed');
  255. DOM.rightSidebar.classList.remove('collapsed');
  256. }
  257. });
  258. toggleLeft.onclick = () => {
  259. DOM.leftSidebar.classList.toggle('collapsed');
  260. };
  261. toggleRight.onclick = () => {
  262. DOM.rightSidebar.classList.toggle('collapsed');
  263. };
  264. }
  265. // 搜索功能
  266. function initSearch() {
  267. const searchContainer = document.getElementById('search-container');
  268. const searchToggleBtn = document.getElementById('search-toggle-btn');
  269. const searchInput = document.getElementById('search-input');
  270. const searchBtn = document.getElementById('search-btn');
  271. const searchResults = document.getElementById('search-results');
  272. const closeSearchBoxBtn = document.getElementById('close-search-box');
  273. // 检查元素是否存在
  274. if (!searchContainer || !searchToggleBtn || !searchInput || !searchBtn || !searchResults || !closeSearchBoxBtn) {
  275. console.error('Search elements not found:', {
  276. searchContainer: !!searchContainer,
  277. searchToggleBtn: !!searchToggleBtn,
  278. searchInput: !!searchInput,
  279. searchBtn: !!searchBtn,
  280. searchResults: !!searchResults,
  281. closeSearchBoxBtn: !!closeSearchBoxBtn
  282. });
  283. return;
  284. }
  285. let searchTimeout;
  286. const SEARCH_HISTORY_KEY = 'cjydocs_search_history';
  287. // 保存搜索历史到localStorage
  288. const saveSearchHistory = (query) => {
  289. if (!query || query.trim().length < 2) return;
  290. try {
  291. let history = JSON.parse(localStorage.getItem(SEARCH_HISTORY_KEY) || '{}');
  292. history[currentCategory] = query;
  293. localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(history));
  294. } catch (err) {
  295. console.error('Failed to save search history:', err);
  296. }
  297. };
  298. // 搜索函数
  299. const performSearch = async () => {
  300. const query = searchInput.value.trim();
  301. if (query.length < 2) {
  302. searchResults.style.display = 'none';
  303. return;
  304. }
  305. // 保存搜索历史
  306. saveSearchHistory(query);
  307. try {
  308. const response = await fetch(
  309. `/api/search/${encodeURIComponent(currentCategory)}?q=${encodeURIComponent(query)}&currentDoc=${encodeURIComponent(currentDoc)}`
  310. );
  311. if (!response.ok) {
  312. const errorData = await response.json().catch(() => ({}));
  313. throw new Error(errorData.error || `搜索失败 (${response.status})`);
  314. }
  315. const data = await response.json();
  316. displaySearchResults(data);
  317. searchResults.style.display = 'block';
  318. } catch (err) {
  319. console.error('Search error:', err);
  320. // 向用户显示错误信息
  321. displaySearchError(err.message || '搜索失败,请稍后重试');
  322. }
  323. };
  324. // 加载搜索历史
  325. const loadSearchHistory = () => {
  326. try {
  327. const history = JSON.parse(localStorage.getItem(SEARCH_HISTORY_KEY) || '{}');
  328. const lastQuery = history[currentCategory];
  329. if (lastQuery) {
  330. searchInput.value = lastQuery;
  331. // 自动搜索上次的内容
  332. performSearch();
  333. }
  334. } catch (err) {
  335. console.error('Failed to load search history:', err);
  336. }
  337. };
  338. // 清除其他分类的搜索历史
  339. const clearOtherCategoryHistory = () => {
  340. try {
  341. let history = JSON.parse(localStorage.getItem(SEARCH_HISTORY_KEY) || '{}');
  342. // 只保留当前分类
  343. history = { [currentCategory]: history[currentCategory] || '' };
  344. localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(history));
  345. } catch (err) {
  346. console.error('Failed to clear search history:', err);
  347. }
  348. };
  349. // 切换搜索框显示/隐藏
  350. searchToggleBtn.addEventListener('click', (e) => {
  351. e.stopPropagation();
  352. console.log('Search button clicked');
  353. const isActive = searchContainer.classList.toggle('active');
  354. console.log('Search container active:', isActive);
  355. if (isActive) {
  356. // 加载搜索历史
  357. loadSearchHistory();
  358. // 聚焦到搜索框
  359. setTimeout(() => searchInput.focus(), 100);
  360. } else {
  361. // 隐藏搜索结果
  362. searchResults.style.display = 'none';
  363. }
  364. });
  365. // 输入时实时搜索(防抖) - 500ms 减少 API 调用
  366. searchInput.addEventListener('input', () => {
  367. clearTimeout(searchTimeout);
  368. searchTimeout = setTimeout(performSearch, 500);
  369. });
  370. // 点击搜索按钮
  371. searchBtn.addEventListener('click', performSearch);
  372. // 按回车搜索
  373. searchInput.addEventListener('keypress', (e) => {
  374. if (e.key === 'Enter') {
  375. performSearch();
  376. }
  377. });
  378. // 关闭搜索框按钮
  379. closeSearchBoxBtn.addEventListener('click', (e) => {
  380. e.stopPropagation();
  381. searchContainer.classList.remove('active');
  382. searchResults.style.display = 'none';
  383. });
  384. // 点击搜索结果外部关闭
  385. searchResults.addEventListener('click', (e) => {
  386. // 如果点击的是搜索结果容器本身(即遮罩背景),关闭搜索
  387. if (e.target === searchResults) {
  388. searchResults.style.display = 'none';
  389. }
  390. });
  391. // 点击外部关闭
  392. document.addEventListener('click', (e) => {
  393. const searchToggle = document.getElementById('search-toggle-btn');
  394. if (!searchResults.contains(e.target) &&
  395. !searchInput.contains(e.target) &&
  396. !searchBtn.contains(e.target) &&
  397. !searchContainer.contains(e.target) &&
  398. !searchToggle.contains(e.target)) {
  399. searchResults.style.display = 'none';
  400. }
  401. });
  402. // 初始化时清除其他分类的历史
  403. clearOtherCategoryHistory();
  404. }
  405. // 显示搜索错误
  406. function displaySearchError(message) {
  407. const currentDocSection = document.querySelector('#current-doc-results .results-list');
  408. const otherDocsSection = document.querySelector('#other-docs-results .results-list');
  409. const searchResults = document.getElementById('search-results');
  410. // 清空之前的结果
  411. currentDocSection.innerHTML = '';
  412. otherDocsSection.innerHTML = '';
  413. // 隐藏分组标题
  414. document.getElementById('current-doc-results').style.display = 'none';
  415. document.getElementById('other-docs-results').style.display = 'none';
  416. // 显示错误信息
  417. currentDocSection.innerHTML = `<p class="search-error" style="color: #d73a49; padding: 20px; text-align: center;">${message}</p>`;
  418. document.getElementById('current-doc-results').style.display = 'block';
  419. // 显示搜索结果面板
  420. searchResults.style.display = 'block';
  421. }
  422. // 显示搜索结果
  423. function displaySearchResults(data) {
  424. const currentDocSection = document.querySelector('#current-doc-results .results-list');
  425. const otherDocsSection = document.querySelector('#other-docs-results .results-list');
  426. // 清空之前的结果
  427. currentDocSection.innerHTML = '';
  428. otherDocsSection.innerHTML = '';
  429. // 隐藏/显示分组标题
  430. document.getElementById('current-doc-results').style.display =
  431. data.currentDoc.length > 0 ? 'block' : 'none';
  432. document.getElementById('other-docs-results').style.display =
  433. data.otherDocs.length > 0 ? 'block' : 'none';
  434. // 渲染当前文档结果
  435. if (data.currentDoc.length > 0) {
  436. data.currentDoc.forEach(doc => {
  437. const docResults = createDocResultElement(doc, true);
  438. currentDocSection.appendChild(docResults);
  439. });
  440. } else {
  441. currentDocSection.innerHTML = '<p class="no-results">当前文档无匹配结果</p>';
  442. }
  443. // 渲染其他文档结果
  444. if (data.otherDocs.length > 0) {
  445. data.otherDocs.forEach(doc => {
  446. const docResults = createDocResultElement(doc, false);
  447. otherDocsSection.appendChild(docResults);
  448. });
  449. } else if (data.currentDoc.length === 0) {
  450. otherDocsSection.innerHTML = '<p class="no-results">未找到匹配结果</p>';
  451. }
  452. }
  453. // 创建搜索结果元素
  454. function createDocResultElement(doc, isCurrent) {
  455. const container = document.createElement('div');
  456. container.className = 'search-result-item';
  457. const header = document.createElement('div');
  458. header.className = 'result-header';
  459. header.innerHTML = `
  460. <span class="result-doc-name">${doc.docName}</span>
  461. <span class="result-count">${doc.matchCount} 个匹配</span>
  462. `;
  463. container.appendChild(header);
  464. // 添加匹配片段
  465. doc.matches.forEach(match => {
  466. const matchItem = document.createElement('div');
  467. matchItem.className = 'result-match';
  468. // 高亮搜索词
  469. const highlightedSnippet = highlightSearchTerm(match.snippet, document.getElementById('search-input').value);
  470. matchItem.innerHTML = `
  471. <div class="match-line-number">行 ${match.line}</div>
  472. <div class="match-snippet">${highlightedSnippet}</div>
  473. `;
  474. // 点击跳转
  475. matchItem.onclick = () => {
  476. // 隐藏搜索框和搜索结果
  477. const searchContainer = document.querySelector('.search-container');
  478. const searchResults = document.getElementById('search-results');
  479. searchContainer.classList.remove('active');
  480. searchResults.style.display = 'none';
  481. if (isCurrent) {
  482. // 当前文档,滚动到对应位置
  483. scrollToSearchMatch(match.fullLine);
  484. } else {
  485. // 其他文档,加载该文档并跳转到具体位置
  486. loadDocument(doc.docName, match.fullLine);
  487. }
  488. };
  489. container.appendChild(matchItem);
  490. });
  491. return container;
  492. }
  493. // 高亮搜索词
  494. function highlightSearchTerm(text, searchTerm) {
  495. const regex = new RegExp(`(${searchTerm})`, 'gi');
  496. return text.replace(regex, '<mark class="search-highlight">$1</mark>');
  497. }
  498. // 滚动到搜索匹配位置
  499. function scrollToSearchMatch(fullLine) {
  500. const content = document.getElementById('markdown-content');
  501. const searchText = fullLine.trim();
  502. if (!searchText) return;
  503. // 使用TreeWalker遍历所有文本节点
  504. const walker = document.createTreeWalker(
  505. content,
  506. NodeFilter.SHOW_TEXT,
  507. null
  508. );
  509. let found = false;
  510. let node;
  511. while (node = walker.nextNode()) {
  512. if (node.textContent.includes(searchText)) {
  513. const parentElement = node.parentElement;
  514. // 滚动到元素
  515. parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
  516. // 临时高亮父元素
  517. parentElement.classList.add('temp-highlight');
  518. setTimeout(() => {
  519. parentElement.classList.remove('temp-highlight');
  520. }, 2000);
  521. found = true;
  522. break;
  523. }
  524. }
  525. // 如果没有找到精确匹配,尝试部分匹配
  526. if (!found) {
  527. const elements = content.querySelectorAll('p, li, td, th, h1, h2, h3, h4, h5, h6, blockquote, pre, code');
  528. for (const element of elements) {
  529. const text = element.textContent.trim();
  530. // 尝试匹配至少50%的内容
  531. if (text.length > 10 && searchText.includes(text.substring(0, Math.min(text.length, 50)))) {
  532. element.scrollIntoView({ behavior: 'smooth', block: 'center' });
  533. // 临时高亮
  534. element.classList.add('temp-highlight');
  535. setTimeout(() => {
  536. element.classList.remove('temp-highlight');
  537. }, 2000);
  538. break;
  539. }
  540. }
  541. }
  542. }
  543. // 设置回到顶部按钮
  544. function setupBackToTop() {
  545. const backToTopBtn = document.getElementById('back-to-top');
  546. const contentArea = document.getElementById('content-area');
  547. if (!backToTopBtn || !contentArea) return;
  548. // 初始状态:隐藏按钮
  549. backToTopBtn.classList.add('hidden');
  550. // 点击按钮滚动到顶部
  551. backToTopBtn.addEventListener('click', () => {
  552. contentArea.scrollTo({
  553. top: 0,
  554. behavior: 'smooth'
  555. });
  556. });
  557. // 监听滚动事件,控制按钮显示/隐藏
  558. let scrollTimeout;
  559. contentArea.addEventListener('scroll', () => {
  560. clearTimeout(scrollTimeout);
  561. scrollTimeout = setTimeout(() => {
  562. if (contentArea.scrollTop > 300) {
  563. backToTopBtn.classList.remove('hidden');
  564. } else {
  565. backToTopBtn.classList.add('hidden');
  566. }
  567. }, 100);
  568. });
  569. }
  570. // 初始化
  571. document.addEventListener('DOMContentLoaded', () => {
  572. loadDocList();
  573. setupSidebarToggles();
  574. initSearch();
  575. setupBackToTop();
  576. });
  577. // 处理浏览器后退/前进
  578. window.addEventListener('popstate', () => {
  579. const { doc } = getQueryParams();
  580. if (doc) {
  581. loadDocument(doc);
  582. }
  583. });