|
|
@@ -1,6 +1,9 @@
|
|
|
let currentCategory = '';
|
|
|
let currentDoc = '';
|
|
|
let docList = [];
|
|
|
+let currentMarkdownContent = ''; // 保存当前文档的markdown源码
|
|
|
+let isEditMode = false; // 是否处于编辑模式
|
|
|
+let savedScrollPosition = 0; // 保存的滚动位置
|
|
|
|
|
|
// DOM 元素缓存
|
|
|
const DOM = {
|
|
|
@@ -9,7 +12,11 @@ const DOM = {
|
|
|
docNav: null,
|
|
|
toc: null,
|
|
|
leftSidebar: null,
|
|
|
- rightSidebar: null
|
|
|
+ rightSidebar: null,
|
|
|
+ editBtn: null,
|
|
|
+ editorContainer: null,
|
|
|
+ markdownEditor: null,
|
|
|
+ editorDocName: null
|
|
|
};
|
|
|
|
|
|
// 配置 marked
|
|
|
@@ -143,6 +150,7 @@ async function loadDocument(docName, scrollToText = null) {
|
|
|
}
|
|
|
|
|
|
currentDoc = docName;
|
|
|
+ currentMarkdownContent = data.content; // 保存markdown源码
|
|
|
|
|
|
// 渲染 Markdown
|
|
|
const html = marked.parse(data.content);
|
|
|
@@ -731,12 +739,291 @@ function setupBackToTop() {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+// 编辑功能
|
|
|
+function setupEditFeature() {
|
|
|
+ // 缓存DOM元素
|
|
|
+ if (!DOM.editBtn) DOM.editBtn = document.getElementById('edit-btn');
|
|
|
+ if (!DOM.editorContainer) DOM.editorContainer = document.getElementById('editor-container');
|
|
|
+ if (!DOM.markdownEditor) DOM.markdownEditor = document.getElementById('markdown-editor');
|
|
|
+ if (!DOM.editorDocName) DOM.editorDocName = document.getElementById('editor-doc-name');
|
|
|
+
|
|
|
+ const saveBtn = document.getElementById('save-btn');
|
|
|
+ const cancelBtn = document.getElementById('cancel-edit-btn');
|
|
|
+ const contentArea = document.getElementById('content-area');
|
|
|
+
|
|
|
+ // 编辑按钮点击事件 - 切换编辑/查看模式
|
|
|
+ DOM.editBtn.addEventListener('click', () => {
|
|
|
+ if (isEditMode) {
|
|
|
+ exitEditMode();
|
|
|
+ } else {
|
|
|
+ enterEditMode();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 取消编辑
|
|
|
+ cancelBtn.addEventListener('click', () => {
|
|
|
+ exitEditMode();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 保存按钮
|
|
|
+ saveBtn.addEventListener('click', async () => {
|
|
|
+ await saveDocument();
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// 进入编辑模式
|
|
|
+function enterEditMode() {
|
|
|
+ if (!currentDoc || !currentMarkdownContent) {
|
|
|
+ alert('请先加载一个文档');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ isEditMode = true;
|
|
|
+
|
|
|
+ // 更新编辑按钮状态
|
|
|
+ DOM.editBtn.classList.add('active');
|
|
|
+ DOM.editBtn.title = '退出编辑';
|
|
|
+
|
|
|
+ // 保存当前滚动位置
|
|
|
+ const contentArea = document.getElementById('content-area');
|
|
|
+ savedScrollPosition = contentArea.scrollTop;
|
|
|
+
|
|
|
+ // 找到当前视口中心附近的元素
|
|
|
+ const visibleElement = findVisibleElement();
|
|
|
+
|
|
|
+ // 隐藏渲染的内容
|
|
|
+ if (!DOM.content) DOM.content = document.getElementById('markdown-content');
|
|
|
+ DOM.content.style.display = 'none';
|
|
|
+
|
|
|
+ // 显示编辑器
|
|
|
+ DOM.editorContainer.style.display = 'flex';
|
|
|
+ DOM.editorDocName.textContent = `${currentDoc}.md`;
|
|
|
+ DOM.markdownEditor.value = currentMarkdownContent;
|
|
|
+
|
|
|
+ // 定位光标到对应位置
|
|
|
+ setTimeout(() => {
|
|
|
+ if (visibleElement) {
|
|
|
+ positionCursorByElement(visibleElement);
|
|
|
+ } else {
|
|
|
+ // 如果找不到元素,使用滚动比例
|
|
|
+ const scrollRatio = savedScrollPosition / contentArea.scrollHeight;
|
|
|
+ DOM.markdownEditor.scrollTop = DOM.markdownEditor.scrollHeight * scrollRatio;
|
|
|
+ }
|
|
|
+ DOM.markdownEditor.focus();
|
|
|
+ }, 50);
|
|
|
+}
|
|
|
+
|
|
|
+// 找到当前视口中可见的元素
|
|
|
+function findVisibleElement() {
|
|
|
+ const contentArea = document.getElementById('content-area');
|
|
|
+ const viewportTop = contentArea.scrollTop;
|
|
|
+ const viewportMiddle = viewportTop + contentArea.clientHeight / 3; // 视口上方1/3处
|
|
|
+
|
|
|
+ // 查找所有重要元素(标题、段落等)
|
|
|
+ const elements = DOM.content.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, blockquote, pre');
|
|
|
+
|
|
|
+ let closestElement = null;
|
|
|
+ let minDistance = Infinity;
|
|
|
+
|
|
|
+ elements.forEach(el => {
|
|
|
+ const rect = el.getBoundingClientRect();
|
|
|
+ const elementTop = el.offsetTop;
|
|
|
+ const distance = Math.abs(elementTop - viewportMiddle);
|
|
|
+
|
|
|
+ if (distance < minDistance && elementTop <= viewportMiddle + 200) {
|
|
|
+ minDistance = distance;
|
|
|
+ closestElement = el;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return closestElement;
|
|
|
+}
|
|
|
+
|
|
|
+// 根据元素定位光标
|
|
|
+function positionCursorByElement(element) {
|
|
|
+ const elementText = element.textContent.trim();
|
|
|
+
|
|
|
+ if (!elementText) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取元素标签类型
|
|
|
+ const tagName = element.tagName.toLowerCase();
|
|
|
+ let searchText = elementText;
|
|
|
+
|
|
|
+ // 如果是标题,添加markdown标记进行搜索
|
|
|
+ if (tagName.match(/^h[1-6]$/)) {
|
|
|
+ const level = parseInt(tagName[1]);
|
|
|
+ const hashes = '#'.repeat(level);
|
|
|
+
|
|
|
+ // 尝试匹配标题行
|
|
|
+ const lines = currentMarkdownContent.split('\n');
|
|
|
+ let targetLine = -1;
|
|
|
+
|
|
|
+ for (let i = 0; i < lines.length; i++) {
|
|
|
+ const line = lines[i].trim();
|
|
|
+ // 匹配 "# 标题" 格式
|
|
|
+ if (line.startsWith(hashes + ' ') && line.substring(level + 1).trim() === elementText) {
|
|
|
+ targetLine = i;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (targetLine !== -1) {
|
|
|
+ setEditorCursorToLine(targetLine);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 对于其他元素,搜索文本内容
|
|
|
+ const lines = currentMarkdownContent.split('\n');
|
|
|
+ for (let i = 0; i < lines.length; i++) {
|
|
|
+ const line = lines[i].trim();
|
|
|
+ // 移除markdown标记进行比较
|
|
|
+ const cleanLine = line.replace(/^[#\-*>]+\s*/, '').trim();
|
|
|
+
|
|
|
+ if (cleanLine.includes(elementText.substring(0, Math.min(50, elementText.length)))) {
|
|
|
+ setEditorCursorToLine(i);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 设置编辑器光标到指定行
|
|
|
+function setEditorCursorToLine(lineNumber) {
|
|
|
+ const lines = currentMarkdownContent.split('\n');
|
|
|
+
|
|
|
+ // 计算目标位置的字符索引
|
|
|
+ let charPosition = 0;
|
|
|
+ for (let i = 0; i < lineNumber && i < lines.length; i++) {
|
|
|
+ charPosition += lines[i].length + 1; // +1 for newline
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置光标位置
|
|
|
+ DOM.markdownEditor.setSelectionRange(charPosition, charPosition);
|
|
|
+
|
|
|
+ // 滚动到光标位置
|
|
|
+ const linesBeforeCursor = lineNumber;
|
|
|
+ const lineHeight = 22; // 大约的行高(px)
|
|
|
+ const targetScrollTop = linesBeforeCursor * lineHeight - DOM.markdownEditor.clientHeight / 3;
|
|
|
+
|
|
|
+ DOM.markdownEditor.scrollTop = Math.max(0, targetScrollTop);
|
|
|
+}
|
|
|
+
|
|
|
+// 退出编辑模式
|
|
|
+function exitEditMode() {
|
|
|
+ isEditMode = false;
|
|
|
+
|
|
|
+ // 更新编辑按钮状态
|
|
|
+ DOM.editBtn.classList.remove('active');
|
|
|
+ DOM.editBtn.title = '编辑文档';
|
|
|
+
|
|
|
+ // 隐藏编辑器
|
|
|
+ DOM.editorContainer.style.display = 'none';
|
|
|
+
|
|
|
+ // 显示渲染的内容
|
|
|
+ DOM.content.style.display = 'block';
|
|
|
+
|
|
|
+ // 恢复滚动位置
|
|
|
+ const contentArea = document.getElementById('content-area');
|
|
|
+ setTimeout(() => {
|
|
|
+ contentArea.scrollTop = savedScrollPosition;
|
|
|
+ }, 0);
|
|
|
+}
|
|
|
+
|
|
|
+// 保存文档
|
|
|
+async function saveDocument() {
|
|
|
+ const newContent = DOM.markdownEditor.value;
|
|
|
+
|
|
|
+ if (!currentCategory || !currentDoc) {
|
|
|
+ alert('无法保存:缺少文档信息');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 禁用保存按钮,防止重复点击
|
|
|
+ const saveBtn = document.getElementById('save-btn');
|
|
|
+ saveBtn.disabled = true;
|
|
|
+ saveBtn.textContent = '保存中...';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch(`/api/doc/${encodeURIComponent(currentCategory)}/${encodeURIComponent(currentDoc)}`, {
|
|
|
+ method: 'PUT',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ content: newContent
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ const errorData = await response.json().catch(() => ({}));
|
|
|
+ throw new Error(errorData.error || `保存失败 (${response.status})`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ // 更新本地的markdown内容
|
|
|
+ currentMarkdownContent = newContent;
|
|
|
+
|
|
|
+ // 重新渲染内容
|
|
|
+ const html = marked.parse(newContent);
|
|
|
+ DOM.content.innerHTML = html;
|
|
|
+
|
|
|
+ // 重新生成TOC
|
|
|
+ generateTOC();
|
|
|
+
|
|
|
+ // 退出编辑模式
|
|
|
+ exitEditMode();
|
|
|
+
|
|
|
+ // 显示成功提示
|
|
|
+ showSuccessMessage('保存成功!');
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Save error:', error);
|
|
|
+ alert(`保存失败:${error.message || '请稍后重试'}`);
|
|
|
+ } finally {
|
|
|
+ // 恢复保存按钮
|
|
|
+ saveBtn.disabled = false;
|
|
|
+ saveBtn.textContent = '保存';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 显示成功提示
|
|
|
+function showSuccessMessage(message) {
|
|
|
+ const toast = document.createElement('div');
|
|
|
+ toast.className = 'success-toast';
|
|
|
+ toast.textContent = message;
|
|
|
+ toast.style.cssText = `
|
|
|
+ position: fixed;
|
|
|
+ top: 20px;
|
|
|
+ right: 20px;
|
|
|
+ background: #28a745;
|
|
|
+ color: white;
|
|
|
+ padding: 12px 24px;
|
|
|
+ border-radius: 6px;
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
+ z-index: 10000;
|
|
|
+ animation: slideInRight 0.3s ease-out;
|
|
|
+ `;
|
|
|
+
|
|
|
+ document.body.appendChild(toast);
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ toast.style.animation = 'slideOutRight 0.3s ease-out';
|
|
|
+ setTimeout(() => {
|
|
|
+ document.body.removeChild(toast);
|
|
|
+ }, 300);
|
|
|
+ }, 2000);
|
|
|
+}
|
|
|
+
|
|
|
// 初始化
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
loadDocList();
|
|
|
setupSidebarToggles();
|
|
|
initSearch();
|
|
|
setupBackToTop();
|
|
|
+ setupEditFeature();
|
|
|
});
|
|
|
|
|
|
// 处理浏览器后退/前进
|