// ==UserScript== // @name DeepSeek 代码块增强 // @namespace https://docs.scriptcat.org/ // @version 2.1.0 // @description 代码块自动折叠,左侧导航面板,点击导航仅滚动定位并高亮 // @author ZZW // @match https://chat.deepseek.com/* // @grant GM_addStyle // @run-at document-end // ==/UserScript== (function() { 'use strict'; // ==================== 全局样式(合并两个脚本的样式) ==================== GM_addStyle(` /* ----- 折叠按钮样式 ----- */ .ds-fold-btn { background: transparent; border: none; border-radius: 12px; font-size: 13px; padding: 4px 8px; cursor: pointer; transition: all 0.2s ease; font-family: system-ui, -apple-system, 'Segoe UI', monospace; user-select: none; display: inline-flex; align-items: center; gap: 2px; margin-left: 0; opacity: 0.7; } .ds-fold-btn:hover { background: rgba(128, 128, 128, 0.2); opacity: 1; } .ds-fold-btn .fold-icon { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; } .ds-fold-btn svg { width: 20px; height: 20px; display: block; } .efa13877 .ds-fold-btn { margin-left: 4px; } /* ----- 导航面板样式 ----- */ @keyframes code-flash-anim { 0% { box-shadow: 0 0 0 0 rgba(77, 107, 254, 0); border-color: transparent; } 20% { box-shadow: 0 0 12px 2px rgba(77, 107, 254, 0.6); border-color: #4d6bfe; } 100% { box-shadow: 0 0 0 0 rgba(77, 107, 254, 0); border-color: transparent; } } .ds-code-flash-active { animation: code-flash-anim 1.5s ease-out forwards; border: 2px solid transparent !important; } .ds-code-nav-panel { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); width: 200px; max-height: 70vh; background: rgba(30, 30, 30, 0.85); backdrop-filter: blur(10px); box-shadow: 0 4px 24px rgba(0,0,0,0.3); border-radius: 12px; border: 1px solid rgba(255,255,255,0.1); z-index: 1000; display: flex; flex-direction: column; overflow: hidden; transition: opacity 0.3s, transform 0.3s; } .ds-code-nav-panel:hover { background: rgba(30, 30, 30, 0.95); box-shadow: 0 4px 30px rgba(0,0,0,0.5); } .ds-code-nav-header { padding: 12px 14px; font-weight: 600; font-size: 13px; color: rgba(255,255,255,0.9); border-bottom: 1px solid rgba(255,255,255,0.1); flex-shrink: 0; display: flex; align-items: center; gap: 6px; } .ds-code-nav-list { flex: 1; overflow-y: auto; padding: 6px 0; } .ds-code-nav-list::-webkit-scrollbar { width: 3px; } .ds-code-nav-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; } .ds-code-nav-item { padding: 8px 14px; cursor: pointer; font-size: 12px; color: rgba(255,255,255,0.7); display: flex; align-items: center; gap: 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-left: 3px solid transparent; } .ds-code-nav-item:hover { background: rgba(77, 107, 254, 0.2); color: #fff; border-left-color: #4d6bfe; } .ds-code-nav-item-icon { font-size: 11px; opacity: 0.6; font-family: monospace; } `); // ==================== 折叠模块 ==================== const foldThreshold = 20; // 自动折叠阈值(行数) const processedAttr = 'data-fold-processed'; // 辅助函数:获取代码行数 function getLineCount(preEl) { const text = preEl.innerText || preEl.textContent || ''; let lines = text.split('\n'); if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop(); return lines.length || 1; } // 折叠代码块(完全隐藏) function collapseBlock(preEl, btn) { if (!preEl.dataset.origDisplay) { preEl.dataset.origDisplay = window.getComputedStyle(preEl).display; } preEl.style.display = 'none'; const iconDiv = btn.querySelector('.fold-icon'); if (iconDiv) iconDiv.innerHTML = ICON_CHEVRON_UP; btn.querySelector('span').textContent = btnTextUnfold; btn.setAttribute('aria-label', '展开代码块'); } // 展开代码块(恢复显示) function expandBlock(preEl, btn) { preEl.style.display = preEl.dataset.origDisplay || ''; const iconDiv = btn.querySelector('.fold-icon'); if (iconDiv) iconDiv.innerHTML = ICON_CHEVRON_DOWN; btn.querySelector('span').textContent = btnTextFold; btn.setAttribute('aria-label', '折叠代码块'); } // 查找按钮容器 function findButtonContainer(preEl) { let parent = preEl.closest('.md-code-block'); if (!parent) return null; let btnGroup = parent.querySelector('.efa13877'); if (btnGroup) return btnGroup; btnGroup = parent.querySelector('[class*="button-group"], [class*="actions"], [class*="buttons"]'); return btnGroup || null; } // 创建折叠按钮 function createFoldButton(preEl) { if (!preEl.dataset.origDisplay) { preEl.dataset.origDisplay = window.getComputedStyle(preEl).display; } const shouldAutoFold = getLineCount(preEl) > foldThreshold; let isFolded = false; if (shouldAutoFold) { preEl.style.display = 'none'; isFolded = true; } const btn = document.createElement('button'); btn.className = 'ds-fold-btn'; const iconDiv = document.createElement('div'); iconDiv.className = 'fold-icon'; iconDiv.innerHTML = isFolded ? ICON_CHEVRON_UP : ICON_CHEVRON_DOWN; const textSpan = document.createElement('span'); textSpan.textContent = isFolded ? btnTextUnfold : btnTextFold; btn.appendChild(iconDiv); btn.appendChild(textSpan); btn.setAttribute('aria-label', isFolded ? '展开代码块' : '折叠代码块'); // 存储按钮到 pre 元素上,供导航模块使用(可选,本版本不自动展开,但保留以备后续) preEl._foldBtn = btn; btn.addEventListener('click', (e) => { e.stopPropagation(); const currentlyFolded = preEl.style.display === 'none'; if (currentlyFolded) { expandBlock(preEl, btn); } else { collapseBlock(preEl, btn); } }); return btn; } // 为单个代码块添加按钮 function addFoldButtonToCodeBlock(preEl) { if (preEl.hasAttribute(processedAttr)) return; const targetContainer = findButtonContainer(preEl); if (targetContainer) { if (targetContainer.querySelector('.ds-fold-btn')) { preEl.setAttribute(processedAttr, 'true'); return; } targetContainer.appendChild(createFoldButton(preEl)); } else { const wrapper = document.createElement('div'); wrapper.className = 'ds-fold-btn-wrapper'; wrapper.style.textAlign = 'right'; wrapper.style.marginBottom = '6px'; wrapper.appendChild(createFoldButton(preEl)); preEl.parentNode.insertBefore(wrapper, preEl); } preEl.setAttribute(processedAttr, 'true'); } // 清理旧的包装器 function removeOldFoldWrappers() { document.querySelectorAll('.ds-fold-btn-wrapper').forEach(w => w.remove()); } // 重置处理标记 function resetProcessedFlags() { document.querySelectorAll(`[${processedAttr}]`).forEach(block => block.removeAttribute(processedAttr)); } // 清理重复按钮 function cleanDuplicateButtons() { document.querySelectorAll('.efa13877').forEach(container => { const btns = container.querySelectorAll('.ds-fold-btn'); if (btns.length > 1) { for (let i = 1; i < btns.length; i++) btns[i].remove(); } }); } // 处理所有代码块 function processAllCodeBlocks() { removeOldFoldWrappers(); resetProcessedFlags(); cleanDuplicateButtons(); document.querySelectorAll('pre').forEach(block => { if (!block.hasAttribute(processedAttr)) { addFoldButtonToCodeBlock(block); } }); } // 监听动态添加的代码块(折叠模块的 Observer) function observeCodeBlocksForFold() { const observer = new MutationObserver((mutations) => { let needProcess = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches && node.matches('pre')) { if (!node.hasAttribute(processedAttr)) { addFoldButtonToCodeBlock(node); } needProcess = true; } if (node.querySelectorAll) { const innerBlocks = node.querySelectorAll('pre'); innerBlocks.forEach(block => { if (!block.hasAttribute(processedAttr)) { addFoldButtonToCodeBlock(block); } }); if (innerBlocks.length) needProcess = true; } } } } } if (needProcess) { processAllCodeBlocks(); } }); observer.observe(document.body, { childList: true, subtree: true }); } // ==================== 导航模块(无自动展开) ==================== const PARENT_SELECTOR = '#root > div > div > div.c3ecdb44 > div._7780f2e > div'; let navPanel = null; let clickPending = false; // 获取所有代码块信息(实时) const getCodeBlocksInfo = () => { const blocks = []; document.querySelectorAll('.md-code-block').forEach((block, index) => { const langSpan = block.querySelector('span.d813de27, .code-language, [class*="language"]'); const lang = langSpan ? langSpan.textContent.trim() : 'Code'; const pre = block.querySelector('pre'); let firstLine = ''; if (pre) { const text = pre.innerText || pre.textContent; firstLine = text.split('\n')[0]?.substring(0, 20) || ''; } blocks.push({ element: block, preElement: pre, // 保存 pre 引用(仅用于显示信息,不用于自动展开) label: `${lang} #${index + 1}`, preview: firstLine ? `// ${firstLine}...` : '' }); }); return blocks; }; // 闪烁高亮(作用于 .md-code-block) const triggerFlash = (element) => { if (!element) return; element.classList.remove('ds-code-flash-active'); void element.offsetWidth; element.classList.add('ds-code-flash-active'); setTimeout(() => element.classList.remove('ds-code-flash-active'), 1500); }; // 渲染导航列表 const renderNavList = () => { if (!navPanel) return; const blocks = getCodeBlocksInfo(); const listContainer = navPanel.querySelector('.ds-code-nav-list'); if (blocks.length === 0) { navPanel.style.display = 'none'; return; } navPanel.style.display = 'flex'; listContainer.innerHTML = ''; blocks.forEach((block, idx) => { const item = document.createElement('div'); item.className = 'ds-code-nav-item'; item.setAttribute('data-code-index', idx); item.title = block.preview || block.label; const icon = document.createElement('span'); icon.className = 'ds-code-nav-item-icon'; icon.textContent = ''; const labelSpan = document.createElement('span'); labelSpan.textContent = block.label; labelSpan.style.overflow = 'hidden'; labelSpan.style.textOverflow = 'ellipsis'; item.appendChild(icon); item.appendChild(labelSpan); item.addEventListener('click', async (e) => { e.stopPropagation(); if (clickPending) return; clickPending = true; try { // 重新获取最新的块信息(避免索引失效) const currentBlocks = getCodeBlocksInfo(); const targetBlock = currentBlocks[parseInt(item.getAttribute('data-code-index'))]; if (!targetBlock || !targetBlock.element) { console.warn('代码块不存在,可能已被移除'); return; } const { element } = targetBlock; // 滚动到视图中央(即使代码块折叠也能定位到容器) element.scrollIntoView({ behavior: 'smooth', block: 'center' }); // 延迟闪烁,确保滚动完成后再高亮 setTimeout(() => triggerFlash(element), 300); } finally { setTimeout(() => { clickPending = false; }, 500); } }); listContainer.appendChild(item); }); }; // 初始化导航面板 const initNavPanel = () => { if (navPanel) return; let parent = document.querySelector(PARENT_SELECTOR); if (!parent) return; if (getComputedStyle(parent).position === 'static') parent.style.position = 'relative'; navPanel = document.createElement('div'); navPanel.className = 'ds-code-nav-panel'; const header = document.createElement('div'); header.className = 'ds-code-nav-header'; header.innerHTML = ` 代码导航`; const listContainer = document.createElement('div'); listContainer.className = 'ds-code-nav-list'; navPanel.appendChild(header); navPanel.appendChild(listContainer); parent.appendChild(navPanel); renderNavList(); }; // 防抖更新导航列表 let updateTimer = null; const debouncedRenderNav = () => { if (updateTimer) clearTimeout(updateTimer); updateTimer = setTimeout(() => { if (navPanel) renderNavList(); updateTimer = null; }, 200); }; // 启动导航模块的观察器(监听 DOM 变化,更新列表) const startNavObserver = () => { // 等待父容器出现 const waitForParent = setInterval(() => { if (document.querySelector(PARENT_SELECTOR)) { initNavPanel(); clearInterval(waitForParent); } }, 500); // 监听变化,更新导航列表 const observer = new MutationObserver(debouncedRenderNav); setTimeout(() => { observer.observe(document.body, { childList: true, subtree: true }); }, 1000); }; // ==================== 全局初始化 ==================== function init() { // 初始化折叠功能 processAllCodeBlocks(); observeCodeBlocksForFold(); // 初始化导航功能(不包含自动展开) startNavObserver(); } // 等待页面加载完成后启动 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // 常量定义(放在最后避免变量提升干扰) const btnTextFold = '折叠'; const btnTextUnfold = '展开'; const ICON_CHEVRON_DOWN = ``; const ICON_CHEVRON_UP = ``; })();