// ==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 = ``;
})();