|
|
@@ -4,6 +4,7 @@ let docList = [];
|
|
|
let currentMarkdownContent = ''; // 保存当前文档的markdown源码
|
|
|
let isEditMode = false; // 是否处于编辑模式
|
|
|
let savedScrollPosition = 0; // 保存的滚动位置
|
|
|
+let currentScrollObserver = null; // 保存当前的滚动监听器
|
|
|
|
|
|
// DOM 元素缓存
|
|
|
const DOM = {
|
|
|
@@ -26,12 +27,16 @@ marked.setOptions({
|
|
|
highlight: function(code, lang) {
|
|
|
if (lang && hljs.getLanguage(lang)) {
|
|
|
try {
|
|
|
- return hljs.highlight(code, { language: lang }).value;
|
|
|
+ const result = hljs.highlight(code, { language: lang });
|
|
|
+ return result.value;
|
|
|
} catch (err) {
|
|
|
console.error('Highlight error:', err);
|
|
|
}
|
|
|
}
|
|
|
- return hljs.highlightAuto(code).value;
|
|
|
+
|
|
|
+ // 自动检测语言
|
|
|
+ const autoResult = hljs.highlightAuto(code);
|
|
|
+ return autoResult.value;
|
|
|
},
|
|
|
breaks: true,
|
|
|
gfm: true
|
|
|
@@ -158,6 +163,67 @@ async function loadDocument(docName, scrollToText = null) {
|
|
|
const html = marked.parse(data.content);
|
|
|
DOM.content.innerHTML = html;
|
|
|
|
|
|
+ // 处理图片路径,将相对路径转换为正确的URL
|
|
|
+ DOM.content.querySelectorAll('img').forEach(img => {
|
|
|
+ const src = img.getAttribute('src');
|
|
|
+ // 如果是相对路径(不以 http/https 开头)
|
|
|
+ if (src && !src.startsWith('http://') && !src.startsWith('https://') && !src.startsWith('/')) {
|
|
|
+ // 使用新的API路径,避免中文路径问题
|
|
|
+ const newSrc = `/api/image/${encodeURIComponent(currentCategory)}/${src}`;
|
|
|
+ img.setAttribute('src', newSrc);
|
|
|
+ // 添加错误处理
|
|
|
+ img.onerror = function() {
|
|
|
+ console.error('Failed to load image:', newSrc);
|
|
|
+ // 显示占位图片或错误提示
|
|
|
+ this.alt = '图片加载失败: ' + src;
|
|
|
+ this.style.border = '1px dashed #ccc';
|
|
|
+ this.style.padding = '10px';
|
|
|
+ this.style.minHeight = '100px';
|
|
|
+ this.style.display = 'block';
|
|
|
+ this.style.backgroundColor = '#f5f5f5';
|
|
|
+ this.style.color = '#666';
|
|
|
+ this.style.textAlign = 'center';
|
|
|
+ this.style.lineHeight = '100px';
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 手动为所有代码块添加 hljs 类并确保高亮(修复 marked 高亮可能失败的问题)
|
|
|
+ DOM.content.querySelectorAll('pre code').forEach((block) => {
|
|
|
+ // 如果代码块没有 hljs 类,添加它
|
|
|
+ if (!block.classList.contains('hljs')) {
|
|
|
+ block.classList.add('hljs');
|
|
|
+ }
|
|
|
+ // 如果没有高亮的标记(没有 span 元素),尝试重新高亮
|
|
|
+ if (!block.querySelector('span')) {
|
|
|
+ const lang = block.className.match(/language-(\w+)/)?.[1];
|
|
|
+ if (lang && hljs.getLanguage(lang)) {
|
|
|
+ try {
|
|
|
+ const highlighted = hljs.highlight(block.textContent, { language: lang });
|
|
|
+ block.innerHTML = highlighted.value;
|
|
|
+ block.classList.add('hljs', `language-${lang}`);
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Error re-highlighting:', err);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 尝试自动检测
|
|
|
+ try {
|
|
|
+ const highlighted = hljs.highlightAuto(block.textContent);
|
|
|
+ block.innerHTML = highlighted.value;
|
|
|
+ block.classList.add('hljs');
|
|
|
+ if (highlighted.language) {
|
|
|
+ block.classList.add(`language-${highlighted.language}`);
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Error auto-highlighting:', err);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 为所有代码块添加复制按钮
|
|
|
+ addCopyButtonsToCodeBlocks();
|
|
|
+
|
|
|
// 生成 TOC
|
|
|
generateTOC();
|
|
|
|
|
|
@@ -191,8 +257,14 @@ function generateTOC() {
|
|
|
// 使用 DOM 缓存
|
|
|
if (!DOM.toc) DOM.toc = document.getElementById('toc');
|
|
|
|
|
|
- const headings = DOM.content.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
|
+ // 确保 DOM.content 也被正确缓存
|
|
|
+ if (!DOM.content) DOM.content = document.getElementById('markdown-content');
|
|
|
+
|
|
|
+ // 先清空旧的TOC内容和事件监听器
|
|
|
DOM.toc.innerHTML = '';
|
|
|
+ DOM.toc.removeEventListener('click', handleTocClick);
|
|
|
+
|
|
|
+ const headings = DOM.content.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
|
|
|
|
if (headings.length === 0) {
|
|
|
DOM.toc.innerHTML = '<p class="toc-empty">本文档没有标题</p>';
|
|
|
@@ -245,6 +317,17 @@ function handleTocClick(e, headingMap) {
|
|
|
|
|
|
// 滚动监听
|
|
|
function setupScrollSpy(headings) {
|
|
|
+ // 清理之前的 observer
|
|
|
+ if (currentScrollObserver) {
|
|
|
+ currentScrollObserver.disconnect();
|
|
|
+ currentScrollObserver = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有标题,直接返回
|
|
|
+ if (!headings || headings.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
let activeLink = null;
|
|
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
|
@@ -267,6 +350,9 @@ function setupScrollSpy(headings) {
|
|
|
});
|
|
|
|
|
|
headings.forEach(heading => observer.observe(heading));
|
|
|
+
|
|
|
+ // 保存当前的 observer 以便后续清理
|
|
|
+ currentScrollObserver = observer;
|
|
|
}
|
|
|
|
|
|
// 更新 URL
|
|
|
@@ -1103,6 +1189,116 @@ function showSuccessMessage(message) {
|
|
|
}, 2000);
|
|
|
}
|
|
|
|
|
|
+// 为代码块添加复制按钮
|
|
|
+function addCopyButtonsToCodeBlocks() {
|
|
|
+ const codeBlocks = document.querySelectorAll('pre');
|
|
|
+
|
|
|
+ codeBlocks.forEach(pre => {
|
|
|
+ // 如果已经有复制按钮,跳过
|
|
|
+ if (pre.querySelector('.code-copy-btn')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建包装容器
|
|
|
+ const wrapper = document.createElement('div');
|
|
|
+ wrapper.className = 'code-block-wrapper';
|
|
|
+
|
|
|
+ // 将pre元素包装起来
|
|
|
+ pre.parentNode.insertBefore(wrapper, pre);
|
|
|
+ wrapper.appendChild(pre);
|
|
|
+
|
|
|
+ // 创建复制按钮
|
|
|
+ const copyBtn = document.createElement('button');
|
|
|
+ copyBtn.className = 'code-copy-btn';
|
|
|
+ copyBtn.innerHTML = `
|
|
|
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
|
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
|
+ </svg>
|
|
|
+ <span>复制</span>
|
|
|
+ `;
|
|
|
+
|
|
|
+ // 添加点击事件
|
|
|
+ copyBtn.addEventListener('click', function() {
|
|
|
+ copyCodeToClipboard(pre, copyBtn);
|
|
|
+ });
|
|
|
+
|
|
|
+ wrapper.appendChild(copyBtn);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// 复制代码到剪贴板
|
|
|
+async function copyCodeToClipboard(preElement, button) {
|
|
|
+ const codeElement = preElement.querySelector('code');
|
|
|
+ if (!codeElement) return;
|
|
|
+
|
|
|
+ // 获取纯文本内容(去除HTML标签)
|
|
|
+ const text = codeElement.textContent || codeElement.innerText;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 使用现代API复制
|
|
|
+ await navigator.clipboard.writeText(text);
|
|
|
+
|
|
|
+ // 显示成功状态
|
|
|
+ showCopySuccess(button);
|
|
|
+
|
|
|
+ } catch (err) {
|
|
|
+ // 降级方案:使用传统方法
|
|
|
+ const textArea = document.createElement('textarea');
|
|
|
+ textArea.value = text;
|
|
|
+ textArea.style.position = 'fixed';
|
|
|
+ textArea.style.left = '-9999px';
|
|
|
+ document.body.appendChild(textArea);
|
|
|
+ textArea.select();
|
|
|
+
|
|
|
+ try {
|
|
|
+ document.execCommand('copy');
|
|
|
+ showCopySuccess(button);
|
|
|
+ } catch (err) {
|
|
|
+ console.error('复制失败:', err);
|
|
|
+ showCopyError(button);
|
|
|
+ } finally {
|
|
|
+ document.body.removeChild(textArea);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 显示复制成功
|
|
|
+function showCopySuccess(button) {
|
|
|
+ // 更新按钮状态
|
|
|
+ const originalHTML = button.innerHTML;
|
|
|
+ button.classList.add('copied');
|
|
|
+ button.innerHTML = `
|
|
|
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
+ <polyline points="20 6 9 17 4 12"></polyline>
|
|
|
+ </svg>
|
|
|
+ <span>已复制</span>
|
|
|
+ `;
|
|
|
+
|
|
|
+ // 2秒后恢复原状
|
|
|
+ setTimeout(() => {
|
|
|
+ button.classList.remove('copied');
|
|
|
+ button.innerHTML = originalHTML;
|
|
|
+ }, 2000);
|
|
|
+}
|
|
|
+
|
|
|
+// 显示复制失败
|
|
|
+function showCopyError(button) {
|
|
|
+ const originalHTML = button.innerHTML;
|
|
|
+ button.innerHTML = `
|
|
|
+ <svg xmlns="http://www.w3.org/2000/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"></circle>
|
|
|
+ <line x1="12" y1="8" x2="12" y2="12"></line>
|
|
|
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
|
+ </svg>
|
|
|
+ <span>复制失败</span>
|
|
|
+ `;
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ button.innerHTML = originalHTML;
|
|
|
+ }, 2000);
|
|
|
+}
|
|
|
+
|
|
|
// 初始化
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
loadDocList();
|