您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在 Codetop 题目列表每行“笔记”按钮旁插入自定义按钮(初版)
// ==UserScript== // @name Codetop Notes 增强 // @namespace http://tampermonkey.net/ // @version 0.3 // @description 在 Codetop 题目列表每行“笔记”按钮旁插入自定义按钮(初版) // @author YourName // @match https://codetop.cc/* // @grant none // ==/UserScript== (function() { 'use strict'; // 工具函数:插入自定义按钮 function insertCustomNoteButtons() { // 兼容所有“笔记”按钮(无论列号、class如何变化) const noteSpans = Array.from(document.querySelectorAll('table tr td .el-button > span')) .filter(span => span.textContent.trim() === '笔记'); noteSpans.forEach(span => { const noteBtn = span.parentElement; const btnGroup = noteBtn.parentElement; // 避免重复插入 if (btnGroup.querySelector('.ctn-custom-note-btn')) { // 按钮已存在,但要更新状态 const existingBtn = btnGroup.querySelector('.ctn-custom-note-btn'); let tr = existingBtn; while (tr && tr.tagName !== 'TR') tr = tr.parentElement; if (tr) { const key = getRowKeyFromBtn(existingBtn); loadNote(key).then(content => { updateButtonState(existingBtn, content); }).catch(err => { // 按钮状态更新失败不影响主要功能 }); } return; } // 创建自定义按钮 const btn = document.createElement('button'); btn.className = noteBtn.className + ' ctn-custom-note-btn'; btn.style.marginLeft = '6px'; btn.innerHTML = '📝'; btn.style.maxWidth = '40px'; btn.style.padding = '0 8px'; btn.style.fontSize = '16px'; btn.style.whiteSpace = 'nowrap'; btn.style.height = noteBtn.offsetHeight + 'px'; btn.title = '自定义笔记'; btn.addEventListener('click', showCustomNoteModal); btnGroup.insertBefore(btn, noteBtn.nextSibling); // 优化:如果该题存在笔记,按钮显示绿色 let tr = btn; while (tr && tr.tagName !== 'TR') tr = tr.parentElement; if (tr) { const key = getRowKeyFromBtn(btn); loadNote(key).then(content => { updateButtonState(btn, content); }).catch(err => { // 按钮状态更新失败不影响主要功能 }); } }); } // IndexedDB 简单封装 const DB_NAME = 'codetop_notes'; const STORE_NAME = 'notes'; // 只做最基础的open,不做任何超时、自动删除、reset、test等 function openDB() { return new Promise((resolve, reject) => { const req = window.indexedDB.open(DB_NAME, 1); req.onupgradeneeded = function(e) { const db = e.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: 'key' }); } }; req.onsuccess = function(e) { resolve(e.target.result); }; req.onerror = function(e) { console.error('数据库打开失败:', e); reject(e); }; req.onblocked = function(e) { console.error('数据库被阻塞:', e); reject(new Error('数据库被阻塞')); }; }); } // 修改 saveNote 支持可选 updated_at 参数 function saveNote(key, content, updated_at) { return openDB().then(db => { return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite'); const store = tx.objectStore(STORE_NAME); const putRequest = store.put({ key, content, updated_at: typeof updated_at === 'number' ? updated_at : Date.now() }); putRequest.onsuccess = () => { resolve(); }; putRequest.onerror = (e) => { console.error('保存笔记失败:', e); reject(e); }; tx.onerror = (e) => { console.error('事务失败:', e); reject(e); }; }); }); } function loadNote(key) { return openDB().then(db => { return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly'); const store = tx.objectStore(STORE_NAME); const req = store.get(key); req.onsuccess = () => { const result = req.result ? req.result.content : ''; resolve(result); }; req.onerror = (e) => { console.error('加载笔记失败:', e); reject(e); }; tx.onerror = (e) => { console.error('事务失败:', e); reject(e); }; }); }); } // 更新按钮状态的工具函数 // ... existing code ... function updateButtonState(btn, content) { if (content && content.trim()) { btn.style.background = '#e6a23c'; // 有内容时橙色 btn.style.color = '#fff'; btn.style.borderColor = '#e6a23c'; } else { // 默认灰色 btn.style.background = '#909399'; btn.style.color = '#fff'; btn.style.borderColor = '#909399'; } } // ... existing code ... // 获取当前行的题目唯一 key function getRowKeyFromBtn(btn) { let tr = btn; while (tr && tr.tagName !== 'TR') tr = tr.parentElement; if (!tr) { return ''; } // 优先用 tr 的 data-row-key 或 data-id if (tr.dataset && (tr.dataset.rowKey || tr.dataset.id)) { return tr.dataset.rowKey || tr.dataset.id; } // 依次检查前两个td,优先用a标签href const tds = tr.querySelectorAll('td'); for (let i = 0; i < Math.min(2, tds.length); i++) { const a = tds[i].querySelector('a'); if (a && a.href) { return a.href; } } // 如果没有a标签,再用前两个td的文本 for (let i = 0; i < Math.min(2, tds.length); i++) { const text = tds[i].textContent.trim(); if (text) { return `${tr.rowIndex || ''}_${text}`; } } // 兜底:用整行文本+行号 const key = `${tr.rowIndex || ''}_${tr.textContent.trim()}`; return key; } // 简单浮层(Modal)实现 function showCustomNoteModal(e) { // 若已存在则不重复弹出 if (document.querySelector('.ctn-modal-mask')) return; const btn = e.currentTarget; const noteKey = getRowKeyFromBtn(btn); // 先渲染 modal 骨架和 loading const mask = document.createElement('div'); mask.className = 'ctn-modal-mask'; mask.style = ` position:fixed;left:0;top:0;width:100vw;height:100vh;background:rgba(0,0,0,0.3);z-index:9999;display:flex;align-items:center;justify-content:center;`; const modal = document.createElement('div'); modal.className = 'ctn-modal'; // 全屏样式 modal.style = ` background:#fff; padding:0; border-radius:0; width:100vw; height:100vh; max-width:100vw; max-height:100vh; box-shadow:none; position:relative; display:flex; flex-direction:row; gap:0; overflow:hidden; `; // 关闭按钮 const closeBtn = document.createElement('span'); closeBtn.innerHTML = '×'; closeBtn.style = 'position:absolute;right:32px;top:24px;font-size:32px;cursor:pointer;z-index:2;color:#d4d4d4;background:rgba(30,30,30,0.8);border-radius:50%;width:40px;height:40px;display:flex;align-items:center;justify-content:center;transition:all 0.2s;'; closeBtn.title = '关闭'; closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = 'rgba(255,255,255,0.1)'; closeBtn.style.color = '#ffffff'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'rgba(30,30,30,0.8)'; closeBtn.style.color = '#d4d4d4'; }); closeBtn.onclick = () => { mask.remove(); document.removeEventListener('keydown', escListener); }; // ESC 键关闭浮层 function escListener(ev) { if (ev.key === 'Escape') { mask.remove(); document.removeEventListener('keydown', escListener); } } document.addEventListener('keydown', escListener); // 左右两栏骨架 const left = document.createElement('div'); left.style = 'flex:5;min-width:0;height:100vh;max-height:100vh;overflow:auto;display:flex;flex-direction:column;padding:48px 32px 32px 48px;box-sizing:border-box;background:#1e1e1e;'; left.innerHTML = '<div style="padding:32px;text-align:center;color:#d4d4d4;">加载编辑器中...</div>'; const right = document.createElement('div'); right.style = 'flex:5;min-width:0;height:100vh;max-height:100vh;overflow:auto;border-left:1px solid #3c3c3c;padding:48px 48px 32px 48px;box-sizing:border-box;background:#2d2d30;color:#d4d4d4;'; right.innerHTML = '<div style="padding:32px;text-align:center;color:#d4d4d4;">加载预览中...</div>'; // 为右侧面板添加自定义滚动条样式 right.style.setProperty('scrollbar-width', 'thin'); right.style.setProperty('scrollbar-color', '#424242 #2d2d30'); // 组装 modal.appendChild(closeBtn); modal.appendChild(left); modal.appendChild(right); mask.appendChild(modal); document.body.appendChild(mask); // 加载依赖后再初始化编辑器和预览 loadEasyMDE(() => { left.innerHTML = ''; right.innerHTML = ''; const textarea = document.createElement('textarea'); textarea.id = 'ctn-md-editor'; // 保存按钮 const saveBtn = document.createElement('button'); saveBtn.textContent = '保存'; saveBtn.style = 'margin:12px 0 0 0;align-self:flex-end;padding:6px 18px;background:#0e639c;color:#fff;border:1px solid #1177bb;border-radius:4px;cursor:pointer;font-size:16px;transition:background 0.2s;'; saveBtn.addEventListener('mouseenter', () => { saveBtn.style.background = '#1177bb'; }); saveBtn.addEventListener('mouseleave', () => { saveBtn.style.background = '#0e639c'; }); // 保存提示 const saveTip = document.createElement('span'); saveTip.style = 'margin-left:12px;color:#4fc1ff;font-size:14px;display:none;'; saveTip.textContent = '已保存!'; left.appendChild(textarea); left.appendChild(saveBtn); left.appendChild(saveTip); right.innerHTML = '<div style="font-weight:bold;margin-bottom:8px;color:#569cd6;font-size:18px;border-bottom:1px solid #3c3c3c;padding-bottom:8px;">📖 实时预览</div><div id="ctn-md-preview" style="min-height:320px;"></div>'; // 初始化 EasyMDE const easyMDE = new window.EasyMDE({ element: textarea, autoDownloadFontAwesome: false, status: false, toolbar: false, // 禁用工具栏,保持简洁 minHeight: '320px', spellChecker: false, placeholder: '请输入 Markdown 笔记...', theme: 'dark', styleSelectedText: false }); // 设置编辑器暗色主题样式 setTimeout(() => { const editor = easyMDE.codemirror; const wrapper = editor.getWrapperElement(); // 设置编辑器暗色主题 wrapper.style.background = '#1e1e1e'; wrapper.style.color = '#d4d4d4'; wrapper.style.border = '1px solid #3c3c3c'; wrapper.style.borderRadius = '6px'; // 设置编辑器内部样式 const editorElement = wrapper.querySelector('.CodeMirror'); if (editorElement) { editorElement.style.background = '#1e1e1e'; editorElement.style.color = '#d4d4d4'; editorElement.style.fontFamily = 'Consolas, "Courier New", monospace'; editorElement.style.fontSize = '14px'; editorElement.style.lineHeight = '1.5'; } // 设置光标颜色 const cursorElements = wrapper.querySelectorAll('.CodeMirror-cursor'); cursorElements.forEach(cursor => { cursor.style.borderColor = '#d4d4d4'; }); // 设置选中文本样式 const style = document.createElement('style'); style.textContent = ` .CodeMirror-dark .CodeMirror-selected { background: #264f78; } .CodeMirror-dark .CodeMirror-line::selection, .CodeMirror-dark .CodeMirror-line > span::selection, .CodeMirror-dark .CodeMirror-line > span > span::selection { background: #264f78; } .CodeMirror-dark .CodeMirror-activeline-background { background: #2a2a2a; } .CodeMirror-dark .CodeMirror-gutters { background: #252526; border-right: 1px solid #3c3c3c; } .CodeMirror-dark .CodeMirror-linenumber { color: #858585; } /* Markdown 语法高亮 */ .CodeMirror-dark .cm-header { color: #569cd6; font-weight: bold; } .CodeMirror-dark .cm-header-1 { color: #569cd6; font-size: 1.4em; } .CodeMirror-dark .cm-header-2 { color: #569cd6; font-size: 1.3em; } .CodeMirror-dark .cm-header-3 { color: #569cd6; font-size: 1.2em; } .CodeMirror-dark .cm-quote { color: #6a9955; font-style: italic; } .CodeMirror-dark .cm-strong { color: #d4d4d4; font-weight: bold; } .CodeMirror-dark .cm-em { color: #d4d4d4; font-style: italic; } .CodeMirror-dark .cm-link { color: #4fc1ff; text-decoration: underline; } .CodeMirror-dark .cm-url { color: #4fc1ff; } .CodeMirror-dark .cm-comment { color: #6a9955; } .CodeMirror-dark .cm-string { color: #ce9178; } .CodeMirror-dark .cm-keyword { color: #569cd6; } .CodeMirror-dark .cm-builtin { color: #dcdcaa; } .CodeMirror-dark .cm-variable-2 { color: #9cdcfe; } .CodeMirror-dark .cm-variable-3 { color: #4ec9b0; } .CodeMirror-dark .cm-tag { color: #569cd6; } .CodeMirror-dark .cm-attribute { color: #9cdcfe; } .CodeMirror-dark .cm-number { color: #b5cea8; } .CodeMirror-dark .cm-atom { color: #569cd6; } .CodeMirror-dark .cm-meta { color: #dcdcaa; } .CodeMirror-dark .cm-bracket { color: #d4d4d4; } /* 代码块样式 */ .CodeMirror-dark .cm-formatting-code-block, .CodeMirror-dark .cm-formatting-code { color: #808080; } .CodeMirror-dark .cm-comment.cm-formatting-code-block { background: #2d2d30; color: #ce9178; border-radius: 3px; padding: 1px 3px; } /* 列表样式 */ .CodeMirror-dark .cm-formatting-list { color: #569cd6; font-weight: bold; } /* 分割线样式 */ .CodeMirror-dark .cm-hr { color: #808080; font-weight: bold; } /* 工具栏隐藏(如果存在) */ .CodeMirror-dark + .editor-toolbar { display: none !important; } /* 滚动条样式 */ .CodeMirror-dark .CodeMirror-scrollbar-filler, .CodeMirror-dark .CodeMirror-gutter-filler { background: #1e1e1e; } .CodeMirror-dark .CodeMirror-scroll::-webkit-scrollbar { width: 10px; height: 10px; } .CodeMirror-dark .CodeMirror-scroll::-webkit-scrollbar-track { background: #2d2d30; } .CodeMirror-dark .CodeMirror-scroll::-webkit-scrollbar-thumb { background: #424242; border-radius: 5px; } .CodeMirror-dark .CodeMirror-scroll::-webkit-scrollbar-thumb:hover { background: #4f4f4f; } /* 焦点样式 */ .CodeMirror-dark.CodeMirror-focused .CodeMirror-selected { background: #264f78; } /* Placeholder 样式 */ .CodeMirror-dark .CodeMirror-placeholder { color: #717171; } .CodeMirror-dark .CodeMirror-empty.CodeMirror-focused .CodeMirror-placeholder { color: #717171; } /* 预览区域暗色主题样式 */ #ctn-md-preview { background: #2d2d30; color: #d4d4d4; border-radius: 6px; padding: 16px; line-height: 1.6; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; } #ctn-md-preview h1, #ctn-md-preview h2, #ctn-md-preview h3, #ctn-md-preview h4, #ctn-md-preview h5, #ctn-md-preview h6 { color: #569cd6; border-bottom: 1px solid #3c3c3c; padding-bottom: 0.3em; margin-top: 24px; margin-bottom: 16px; } #ctn-md-preview h1 { font-size: 2em; } #ctn-md-preview h2 { font-size: 1.5em; } #ctn-md-preview h3 { font-size: 1.25em; } #ctn-md-preview h4 { font-size: 1em; } #ctn-md-preview h5 { font-size: 0.875em; } #ctn-md-preview h6 { font-size: 0.85em; } #ctn-md-preview p { margin-bottom: 16px; color: #d4d4d4; } #ctn-md-preview code { background: #1e1e1e; color: #f8f8f2; padding: 2px 6px; border-radius: 3px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 0.875em; border: 1px solid #3c3c3c; } #ctn-md-preview pre { background: #1e1e1e; border: 1px solid #3c3c3c; border-radius: 6px; padding: 16px; overflow-x: auto; margin: 16px 0; } #ctn-md-preview pre code { background: transparent; border: none; padding: 0; color: inherit; } #ctn-md-preview blockquote { background: #2a2a2a; border-left: 4px solid #6a9955; padding: 8px 16px; margin: 16px 0; color: #d4d4d4; font-style: italic; } #ctn-md-preview ul, #ctn-md-preview ol { padding-left: 24px; margin: 16px 0; } #ctn-md-preview li { margin: 4px 0; color: #d4d4d4; } #ctn-md-preview a { color: #4fc1ff; text-decoration: none; border-bottom: 1px solid transparent; transition: border-color 0.2s; } #ctn-md-preview a:hover { border-bottom-color: #4fc1ff; } #ctn-md-preview table { border-collapse: collapse; width: 100%; margin: 16px 0; background: #252526; border: 1px solid #3c3c3c; border-radius: 6px; overflow: hidden; } #ctn-md-preview th, #ctn-md-preview td { border: 1px solid #3c3c3c; padding: 8px 12px; text-align: left; } #ctn-md-preview th { background: #1e1e1e; color: #569cd6; font-weight: bold; } #ctn-md-preview td { color: #d4d4d4; } #ctn-md-preview hr { border: none; border-top: 2px solid #3c3c3c; margin: 24px 0; } #ctn-md-preview img { max-width: 100%; height: auto; border-radius: 6px; border: 1px solid #3c3c3c; } #ctn-md-preview strong { color: #e6db74; font-weight: bold; } #ctn-md-preview em { color: #ae81ff; font-style: italic; } /* 自定义滚动条 - 预览区域 */ #ctn-md-preview::-webkit-scrollbar { width: 10px; } #ctn-md-preview::-webkit-scrollbar-track { background: #2d2d30; } #ctn-md-preview::-webkit-scrollbar-thumb { background: #424242; border-radius: 5px; } #ctn-md-preview::-webkit-scrollbar-thumb:hover { background: #4f4f4f; } /* 右侧面板滚动条样式 */ .ctn-modal div[style*="background:#2d2d30"]::-webkit-scrollbar { width: 12px; } .ctn-modal div[style*="background:#2d2d30"]::-webkit-scrollbar-track { background: #2d2d30; } .ctn-modal div[style*="background:#2d2d30"]::-webkit-scrollbar-thumb { background: #424242; border-radius: 6px; } .ctn-modal div[style*="background:#2d2d30"]::-webkit-scrollbar-thumb:hover { background: #4f4f4f; } `; document.head.appendChild(style); // 应用暗色主题类 wrapper.classList.add('CodeMirror-dark'); // 启用markdown模式和语法高亮 const mode = window.CodeMirror && window.CodeMirror.modes && window.CodeMirror.modes.gfm ? 'gfm' : window.CodeMirror && window.CodeMirror.modes && window.CodeMirror.modes.markdown ? 'markdown' : 'text/plain'; editor.setOption('mode', mode); editor.setOption('theme', 'default'); editor.setOption('lineNumbers', false); editor.setOption('lineWrapping', true); editor.setOption('highlightFormatting', true); editor.setOption('tokenTypeOverrides', { header: 'header', quote: 'quote', list1: 'variable-2', list2: 'variable-3', list3: 'keyword', hr: 'hr', image: 'tag', formatting: 'meta', linkInline: 'link', linkEmail: 'link', linkText: 'link', linkHref: 'string' }); // 刷新编辑器 editor.refresh(); }, 100); // 加载笔记内容 loadNote(noteKey).then(content => { easyMDE.value(content); updatePreview(); }); // 实时预览 function updatePreview() { const md = easyMDE.value(); let renderMarkdown = md => md; if (window.marked) { renderMarkdown = typeof window.marked === 'function' ? window.marked : (window.marked.marked ? window.marked.marked : renderMarkdown); } const previewContainer = document.getElementById('ctn-md-preview'); previewContainer.innerHTML = renderMarkdown(md); // 应用代码高亮 previewContainer.querySelectorAll('pre code').forEach(block => { block.classList.add('hljs'); if (window.hljs && typeof window.hljs.highlightElement === 'function') { // 清除之前的高亮 block.removeAttribute('data-highlighted'); window.hljs.highlightElement(block); } }); // 如果没有内容,显示提示 if (!md.trim()) { previewContainer.innerHTML = '<div style="text-align:center;color:#858585;padding:40px;font-style:italic;">✍️ 在左侧编辑器中输入 Markdown 内容,这里会实时显示预览效果</div>'; } } easyMDE.codemirror.on('change', updatePreview); // 保存按钮事件 saveBtn.onclick = () => { const val = easyMDE.value(); saveNote(noteKey, val).then(() => { saveTip.style.display = ''; setTimeout(() => { saveTip.style.display = 'none'; }, 1200); // 保存成功后更新按钮状态 updateButtonState(btn, val); }).catch(err => { console.error('保存失败:', err); alert('保存失败,请重试'); }); }; }); } // 动态加载 EasyMDE、marked、highlight.js function loadEasyMDE(cb) { ensureFontAwesome(); // 先加载 CodeMirror markdown 模式 loadCodeMirrorMarkdown(() => { // EasyMDE if (!window.EasyMDE) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css'; document.head.appendChild(link); const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js'; script.onload = () => { loadMarked(cb); }; script.onerror = () => { alert('EasyMDE 加载失败,请检查网络'); }; document.body.appendChild(script); } else { loadMarked(cb); } }); } // 加载 CodeMirror markdown 模式 function loadCodeMirrorMarkdown(cb) { if (window.CodeMirror && window.CodeMirror.modes && window.CodeMirror.modes.markdown) { cb(); return; } // 加载 CodeMirror markdown 模式 const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/mode/markdown/markdown.min.js'; script.onload = () => { // 加载 CodeMirror overlay 模式(markdown依赖) const overlayScript = document.createElement('script'); overlayScript.src = 'https://cdn.jsdelivr.net/npm/[email protected]/addon/mode/overlay.min.js'; overlayScript.onload = () => { // 加载 GFM 模式 const gfmScript = document.createElement('script'); gfmScript.src = 'https://cdn.jsdelivr.net/npm/[email protected]/mode/gfm/gfm.min.js'; gfmScript.onload = cb; gfmScript.onerror = cb; // 即使加载失败也继续 document.body.appendChild(gfmScript); }; overlayScript.onerror = cb; document.body.appendChild(overlayScript); }; script.onerror = cb; document.body.appendChild(script); } // 动态引入 FontAwesome 图标库 function ensureFontAwesome() { if (!document.querySelector('link[href*="font-awesome"]')) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://cdn.jsdelivr.net/npm/[email protected]/css/font-awesome.min.css'; document.head.appendChild(link); } } function loadMarked(cb) { if (!window.marked) { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js'; script.onload = () => { loadHLJS(cb); }; script.onerror = () => { alert('marked 加载失败,请检查网络'); }; document.body.appendChild(script); } else { loadHLJS(cb); } } function loadHLJS(cb) { if (!window.hljs) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://unpkg.com/@highlightjs/[email protected]/styles/vs2015.min.css'; // 使用暗色主题 document.head.appendChild(link); const script = document.createElement('script'); script.src = 'https://unpkg.com/@highlightjs/[email protected]/highlight.min.js'; script.onload = () => { // 配置 marked 的 highlight 选项 if (window.marked && window.hljs) { window.marked.setOptions({ highlight: function(code, lang) { if (window.hljs.getLanguage(lang)) { return window.hljs.highlight(code, { language: lang }).value; } return window.hljs.highlightAuto(code).value; } }); } cb(); }; script.onerror = () => { alert('highlight.js 加载失败,请检查网络'); }; document.body.appendChild(script); } else { // 配置 marked 的 highlight 选项 if (window.marked && window.hljs) { window.marked.setOptions({ highlight: function(code, lang) { if (window.hljs.getLanguage(lang)) { return window.hljs.highlight(code, { language: lang }).value; } return window.hljs.highlightAuto(code).value; } }); } cb(); } } // 监听表格变化,保证按钮持续插入 function observeTable() { // 监听整个页面的变化,不只是表格 const targetNode = document.body; const observer = new MutationObserver((mutations) => { let shouldUpdate = false; mutations.forEach((mutation) => { // 检查是否有新增的节点包含表格行 if (mutation.type === 'childList') { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { // 检查是否是表格相关的变化 if (node.classList?.contains('el-table__body') || node.querySelector?.('.el-table__body') || node.querySelector?.('td.el-table_1_column_6') || node.tagName === 'TR' || node.classList?.contains('el-table__row') || node.querySelector?.('.el-table__row')) { shouldUpdate = true; } } }); // 检查移除的节点 mutation.removedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName === 'TR' || node.classList?.contains('el-table__row')) { shouldUpdate = true; } } }); } // 检查属性变化(可能的翻页触发) if (mutation.type === 'attributes' && (mutation.attributeName === 'class' || mutation.attributeName === 'style' || mutation.attributeName === 'data-key')) { const target = mutation.target; if (target.classList?.contains('el-table') || target.closest?.('.el-table') || target.classList?.contains('el-pagination') || target.closest?.('.el-pagination')) { shouldUpdate = true; } } }); if (shouldUpdate) { // 延迟执行,确保DOM完全更新 setTimeout(() => { insertCustomNoteButtons(); }, 100); // 再次更新确保状态正确 setTimeout(() => { insertCustomNoteButtons(); }, 300); } }); observer.observe(targetNode, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'] }); // 额外监听分页按钮点击 observePaginationClicks(); // 定期检查(后备方案) setInterval(() => { insertCustomNoteButtons(); }, 2000); // 每2秒检查一次,增加频率 } // 监听分页按钮点击 function observePaginationClicks() { // 监听分页相关的点击事件 document.addEventListener('click', (e) => { const target = e.target; // 检查是否点击了分页相关按钮 if (target.closest('.el-pagination') || target.closest('.el-pager') || target.classList.contains('btn-prev') || target.classList.contains('btn-next') || target.classList.contains('number') || target.closest('.el-pagination__jump') || target.closest('.el-pagination__sizes')) { // 立即尝试更新一次 setTimeout(() => { insertCustomNoteButtons(); }, 500); // 再次延迟更新(确保加载完成) setTimeout(() => { insertCustomNoteButtons(); }, 1000); // 最后一次更新(确保状态正确) setTimeout(() => { insertCustomNoteButtons(); }, 1500); } }); // 监听键盘事件(可能的分页快捷键) document.addEventListener('keydown', (e) => { if (e.key === 'PageUp' || e.key === 'PageDown' || (e.key === 'Enter' && e.target.closest('.el-pagination'))) { setTimeout(() => { insertCustomNoteButtons(); }, 800); } }); // 监听URL变化(可能的路由变化) let currentUrl = window.location.href; setInterval(() => { if (window.location.href !== currentUrl) { currentUrl = window.location.href; setTimeout(() => { insertCustomNoteButtons(); }, 1000); } }, 1000); } // 初始化 function init() { insertCustomNoteButtons(); observeTable(); } // 页面插入导出/导入按钮区(只保留主按钮) function insertExportButton() { if (document.querySelector('.ctn-export-notes-btn-group')) return; const group = document.createElement('div'); group.className = 'ctn-export-notes-btn-group'; group.style = ` position:fixed; right:36px; bottom:36px; z-index:10000; display:flex; flex-direction:column; gap:18px; align-items:flex-end; `; // 导出按钮 const exportBtn = document.createElement('button'); exportBtn.className = 'ctn-export-notes-btn'; exportBtn.textContent = '导出笔记'; exportBtn.style = btnStyle(); exportBtn.onclick = exportAllNotes; // codetop导入按钮 const importCodetopBtn = document.createElement('button'); importCodetopBtn.className = 'ctn-import-codetop-btn'; importCodetopBtn.textContent = 'codetop官方笔记 导入'; importCodetopBtn.style = btnStyle('#67c23a'); importCodetopBtn.onclick = showImportCodetopModal; // 插件导入按钮 const importPluginBtn = document.createElement('button'); importPluginBtn.className = 'ctn-import-plugin-btn'; importPluginBtn.textContent = '插件笔记 导入'; importPluginBtn.style = btnStyle('#e6a23c'); importPluginBtn.onclick = showImportPluginModal; // 云同步按钮 const syncBtn = document.createElement('button'); syncBtn.className = 'ctn-sync-notes-btn'; syncBtn.textContent = '云同步'; syncBtn.style = btnStyle('#f56c6c'); syncBtn.onclick = mergeSyncAllNotes; // 组装 group.appendChild(exportBtn); group.appendChild(importCodetopBtn); group.appendChild(importPluginBtn); group.appendChild(syncBtn); document.body.appendChild(group); } function btnStyle(bg) { return ` background:${bg || '#409EFF'}; color:#fff; border:none; border-radius:24px; padding:12px 28px; font-size:18px; box-shadow:0 2px 8px rgba(0,0,0,0.12); cursor:pointer; `; } // codetop导入弹窗 function showImportCodetopModal() { showImportModal('codetop'); } // 插件导入弹窗 function showImportPluginModal() { showImportModal('plugin'); } // 通用导入弹窗 function showImportModal(type) { if (document.querySelector('.ctn-modal-mask')) { return; } // 遮罩 const mask = document.createElement('div'); mask.className = 'ctn-modal-mask'; mask.style = 'position:fixed;left:0;top:0;width:100vw;height:100vh;background:rgba(0,0,0,0.3);z-index:99999;display:flex;align-items:center;justify-content:center;'; // 弹窗 const modal = document.createElement('div'); modal.className = 'ctn-modal'; modal.style = 'background:#fff;padding:32px 32px 24px 32px;border-radius:12px;min-width:420px;max-width:90vw;box-shadow:0 2px 16px rgba(0,0,0,0.15);position:relative;'; // 关闭按钮 const closeBtn = document.createElement('span'); closeBtn.innerHTML = '×'; closeBtn.style = 'position:absolute;right:18px;top:12px;font-size:28px;cursor:pointer;z-index:2;'; closeBtn.title = '关闭'; closeBtn.onclick = () => mask.remove(); // 标题 const title = document.createElement('div'); title.style = 'font-size:20px;font-weight:bold;margin-bottom:18px;'; title.textContent = type === 'codetop' ? '从 codetop 导入笔记' : '从插件导入笔记'; // 内容区 const content = document.createElement('div'); content.style = 'margin-bottom:18px;'; if (type === 'codetop') { content.innerHTML = '<textarea style="width:100%;height:120px;font-size:16px;padding:8px;box-sizing:border-box;resize:vertical;" placeholder="粘贴 codetop API 返回的 JSON 或 JSON 数组..."></textarea>'; } else { content.innerHTML = '<input type="file" accept="application/json" style="font-size:16px;">'; } // 导入按钮 const importBtn = document.createElement('button'); importBtn.textContent = '导入'; importBtn.style = 'margin-top:8px;padding:8px 32px;background:#409EFF;color:#fff;border:none;border-radius:6px;font-size:16px;cursor:pointer;'; // 提示 const tip = document.createElement('div'); tip.style = 'margin-top:12px;color:#67c23a;font-size:15px;display:none;'; tip.textContent = '导入成功!'; // 组装 modal.appendChild(closeBtn); modal.appendChild(title); modal.appendChild(content); modal.appendChild(importBtn); modal.appendChild(tip); mask.appendChild(modal); document.body.appendChild(mask); // 导入逻辑 importBtn.onclick = () => { // 保存原始状态 const originalText = importBtn.textContent; const originalBackground = importBtn.style.background; // 设置导入中状态 importBtn.disabled = true; importBtn.textContent = '导入中...'; importBtn.style.background = '#909399'; // 灰色 importBtn.style.cursor = 'not-allowed'; importBtn.style.opacity = '0.6'; // 恢复按钮状态的函数 const restoreButton = () => { importBtn.disabled = false; importBtn.textContent = originalText; importBtn.style.background = originalBackground; importBtn.style.cursor = 'pointer'; importBtn.style.opacity = '1'; }; if (type === 'codetop') { const val = content.querySelector('textarea').value; if (!val.trim()) { alert('请输入要导入的JSON数据'); restoreButton(); return; } let arr; try { arr = JSON.parse(val); } catch (e) { console.error('JSON解析失败:', e); alert('JSON 格式错误: ' + e.message); restoreButton(); return; } if (!Array.isArray(arr)) arr = [arr]; batchImportNotes(arr, type, tip, restoreButton); } else { const file = content.querySelector('input[type=file]').files[0]; if (!file) { alert('请选择文件'); restoreButton(); return; } const reader = new FileReader(); reader.onload = function(e) { let arr; try { arr = JSON.parse(e.target.result); } catch (err) { console.error('文件JSON解析失败:', err); alert('JSON 格式错误: ' + err.message); restoreButton(); return; } if (!Array.isArray(arr)) arr = [arr]; batchImportNotes(arr, type, tip, restoreButton); }; reader.onerror = function(e) { console.error('文件读取失败:', e); alert('文件读取失败'); restoreButton(); }; reader.readAsText(file); } }; } // 批量导入 function batchImportNotes(arr, type, tip, callback) { // 先检查 IndexedDB 是否可用 openDB().then(() => { return openDB(); }).then(db => { const tx = db.transaction(STORE_NAME, 'readwrite'); const store = tx.objectStore(STORE_NAME); let count = 0; let importedKeys = []; let processedCount = 0; let errors = []; if (arr.length === 0) { tip.textContent = '没有找到可导入的数据'; tip.style.display = ''; setTimeout(() => { tip.style.display = 'none'; }, 3000); if (callback) callback(); return; } arr.forEach((item, index) => { let key, content; if (type === 'codetop') { const slug = item.leetcodeInfo && item.leetcodeInfo.slug_title; if (!slug) { processedCount++; checkComplete(); return; } key = `https://leetcode.cn/problems/${slug}`; content = item.content || ''; } else { key = item.key || ''; content = item.content || ''; } if (key) { // 只要有key就尝试导入,即使content为空 const putRequest = store.put({ key, content: content || '', // 确保content不为undefined updated_at: Date.now(), ...(item.leetcodeInfo ? { leetcodeInfo: item.leetcodeInfo } : {}) }); putRequest.onsuccess = () => { importedKeys.push(key); count++; processedCount++; checkComplete(); }; putRequest.onerror = (e) => { errors.push(`${key}: ${e.message || e}`); processedCount++; checkComplete(); }; } else { processedCount++; checkComplete(); } }); function checkComplete() { if (processedCount === arr.length) { let message = `导入完成!成功导入 ${count} 条,跳过 ${arr.length - count} 条。`; if (errors.length > 0) { message += `\n错误 ${errors.length} 条`; } tip.textContent = message; tip.style.color = count > 0 ? '#67c23a' : '#f56c6c'; tip.style.display = ''; tip.style.whiteSpace = 'pre-line'; // 支持换行显示 setTimeout(() => { tip.style.display = 'none'; }, 4000); // 导入完成后刷新按钮状态 setTimeout(() => { insertCustomNoteButtons(); }, 100); if (callback) callback(); } } tx.onerror = (e) => { console.error('事务失败:', e); tip.textContent = '导入失败,请重试'; tip.style.color = '#f56c6c'; tip.style.display = ''; setTimeout(() => { tip.style.display = 'none'; }, 3000); if (callback) callback(); }; }).catch(err => { console.error('打开数据库失败:', err); tip.textContent = '数据库错误,请重试'; tip.style.color = '#f56c6c'; tip.style.display = ''; setTimeout(() => { tip.style.display = 'none'; }, 3000); if (callback) callback(); }); } // 导出所有笔记为 JSON 文件 function exportAllNotes() { // 获取导出按钮并设置状态 const exportBtn = document.querySelector('.ctn-export-notes-btn'); const originalText = exportBtn ? exportBtn.textContent : '导出笔记'; const originalStyle = exportBtn ? exportBtn.style.cssText : ''; if (exportBtn) { exportBtn.textContent = '导出中...'; exportBtn.disabled = true; exportBtn.style.background = '#909399'; // 灰色 exportBtn.style.cursor = 'not-allowed'; exportBtn.style.opacity = '0.6'; } const restoreButton = () => { if (exportBtn) { exportBtn.textContent = originalText; exportBtn.disabled = false; exportBtn.style.cssText = originalStyle; } }; openDB().then(db => { const tx = db.transaction(STORE_NAME, 'readonly'); const store = tx.objectStore(STORE_NAME); const req = store.getAll(); req.onsuccess = () => { const data = req.result || []; if (data.length === 0) { alert('没有笔记可以导出'); restoreButton(); return; } const json = JSON.stringify(data, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const date = new Date().toISOString().slice(0, 10); a.href = url; a.download = `codetop_notes_${date}.json`; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); restoreButton(); // 导出完成后恢复按钮 }, 100); }; req.onerror = (e) => { console.error('导出失败:', e); alert('导出失败,请重试'); restoreButton(); }; }).catch(err => { console.error('打开数据库失败:', err); alert('数据库错误,无法导出'); restoreButton(); }); } // 新增:key 哈希函数(SHA-256) async function hashKey(str) { const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)); return Array.from(new Uint8Array(buf)).map(x => x.toString(16).padStart(2, '0')).join(''); } // 云端获取笔记(用哈希key,支持 updatedTime) async function fetchNoteFromCloud(key) { const hash = await hashKey(key); return fetch(`https://paste.tans.fun/api/note/${encodeURIComponent(hash)}`) .then(res => res.json()) .then(json => { if (json.code === 0 && json.data && typeof json.data.value === 'string') { let updated_at = 0; if (json.data.updatedTime) { // 处理云端时间:云端存储的是 +8 时区的时间戳,需要转换为 UTC let timestamp = new Date(json.data.updatedTime).getTime(); if (!isNaN(timestamp)) { // 减去8小时转换为 UTC 时间戳 timestamp = timestamp - (8 * 60 * 60 * 1000); } updated_at = isNaN(timestamp) ? Date.now() : timestamp; } return { key: json.data.key, content: json.data.value, updated_at }; } return null; }).catch(() => null); } // 云端保存笔记(返回 updatedTime) async function saveNoteToCloud(key, value) { if (!key || typeof key !== 'string' || !key.trim()) { console.error('云同步失败:key 不合法', key); alert('云同步失败:key 不合法,已跳过该条笔记'); return Promise.resolve(false); } const hash = await hashKey(key); return fetch('https://paste.tans.fun/api/note', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({key: hash, value, url: key}) }).then(res => res.json()) .then(json => { if (json.code === 0) { if (json.data && json.data.updatedTime) { // 处理云端返回时间:云端存储的是 +8 时区的时间戳,需要转换为 UTC let serverTime = new Date(json.data.updatedTime).getTime(); if (!isNaN(serverTime)) { // 减去8小时转换为 UTC 时间戳 serverTime = serverTime - (8 * 60 * 60 * 1000); } const finalTime = isNaN(serverTime) ? Date.now() : serverTime; return finalTime; } // 没有 updatedTime,返回当前本地时间 return Date.now(); } throw new Error(json.message || '云端保存失败'); }); } // 合并同步主逻辑(云端操作用哈希key) async function mergeSyncAllNotes() { // 获取同步按钮并设置为禁用状态 const syncBtn = document.querySelector('.ctn-sync-notes-btn'); const originalText = syncBtn ? syncBtn.textContent : '云同步'; const originalStyle = syncBtn ? syncBtn.style.cssText : ''; if (syncBtn) { syncBtn.textContent = '同步中...'; syncBtn.disabled = true; syncBtn.style.background = '#909399'; // 灰色 syncBtn.style.cursor = 'not-allowed'; syncBtn.style.opacity = '0.6'; } try { const db = await openDB(); const tx = db.transaction(STORE_NAME, 'readonly'); const store = tx.objectStore(STORE_NAME); const localNotes = await new Promise((resolve, reject) => { const req = store.getAll(); req.onsuccess = () => resolve(req.result || []); req.onerror = () => reject(new Error('读取本地笔记失败')); }); if (localNotes.length === 0) { alert('本地没有笔记可同步'); return; } let updatedCount = 0, uploadedCount = 0, skippedCount = 0; const totalNotes = localNotes.length; for (let i = 0; i < localNotes.length; i++) { const note = localNotes[i]; const key = note.key; const localContent = note.content; const localUpdated = note.updated_at || 0; // 更新按钮显示进度 if (syncBtn) { syncBtn.textContent = `同步中... (${i + 1}/${totalNotes})`; } // key 校验和调试输出 if (!key || typeof key !== 'string' || !key.trim()) { skippedCount++; continue; } // 拉取云端(用哈希key) const cloudNote = await fetchNoteFromCloud(key); let cloudUpdated = 0; if (cloudNote && typeof cloudNote.updated_at === 'number') { cloudUpdated = cloudNote.updated_at; } // 详细日志输出:本地和云端更新时间(已转换为UTC) if (!cloudNote || !cloudNote.content) { // 云端无内容,上传本地 const serverTime = await saveNoteToCloud(key, localContent); if (serverTime) { await saveNote(key, localContent, serverTime); // 用云端时间更新本地 } uploadedCount++; } else { // 比较时间戳(添加容错机制:如果时间差小于1分钟则认为相同) const timeDiff = Math.abs(cloudUpdated - localUpdated); const isTimeSimilar = timeDiff < 60000; // 1分钟内认为相同 if (isTimeSimilar) { skippedCount++; } else if (localUpdated > cloudUpdated) { // 本地较新,上传 const serverTime = await saveNoteToCloud(key, localContent); if (serverTime) { await saveNote(key, localContent, serverTime); // 用云端时间更新本地 } uploadedCount++; } else if (cloudUpdated > localUpdated) { // 云端较新,写回本地 await saveNote(key, cloudNote.content, cloudNote.updated_at); updatedCount++; } else { // 一致,跳过 skippedCount++; } } } alert(`云同步完成!上传${uploadedCount}条,下载${updatedCount}条,跳过${skippedCount}条。`); } catch (error) { console.error('云同步失败:', error); alert('云同步失败:' + error.message); } finally { // 恢复按钮状态 if (syncBtn) { syncBtn.textContent = originalText; syncBtn.disabled = false; syncBtn.style.cssText = originalStyle; } } } // 页面加载后执行 window.addEventListener('load', () => { setTimeout(init, 300); // 延迟,确保表格渲染 setTimeout(insertExportButton, 1200); // 插入导出/导入按钮 }); })();