|
| 1 | +# docusaurus配置代码块下载按钮 |
| 2 | + |
| 3 | +[download-button插件](https://prismjs.com/plugins/download-button/) |
| 4 | + |
| 5 | +编辑 `src/theme/Root.js` 新增如下内容 |
| 6 | + |
| 7 | +:::tip 说明 |
| 8 | + |
| 9 | +主要实现了以下功能 |
| 10 | + |
| 11 | +- 通过 `querySelectorAll('pre:not(.pre-wrapper)')` 找到页面中的所有 `<pre>` 代码块; |
| 12 | + |
| 13 | +- 为每个代码块动态添加一个“下载”按钮; |
| 14 | + |
| 15 | +- 鼠标悬停时按钮显示,移开后自动隐藏; |
| 16 | + |
| 17 | +- 自动识别代码语言(如 `language-js`),并根据语言类型生成对应后缀; |
| 18 | + |
| 19 | +- 使用 `Blob` + `a.download` 实现前端文件下载; |
| 20 | + |
| 21 | +- 通过 `MutationObserver` 确保在 Docusaurus 页面切换后仍能自动生效; |
| 22 | + |
| 23 | +- 保持与原生“复制代码”按钮的样式风格一致,不会遮挡或干扰。 |
| 24 | + |
| 25 | +::: |
| 26 | + |
| 27 | +```js |
| 28 | +export default function Root({ children }) { |
| 29 | + useEffect(() => { |
| 30 | + ...... |
| 31 | + |
| 32 | + // ------------------------------- |
| 33 | + // 添加下载按钮逻辑(核心部分) |
| 34 | + // ------------------------------- |
| 35 | + function addDownloadButtons() { |
| 36 | + // 选中页面上所有 <pre> 元素(代码块),排除已经包裹过的 .pre-wrapper |
| 37 | + const pres = document.querySelectorAll('pre:not(.pre-wrapper)'); |
| 38 | + |
| 39 | + pres.forEach(pre => { |
| 40 | + // 防止重复包裹 |
| 41 | + if (pre.classList.contains('pre-wrapper')) return; |
| 42 | + pre.classList.add('pre-wrapper'); |
| 43 | + pre.style.position = 'relative'; // 让绝对定位的按钮能相对代码块定位 |
| 44 | + |
| 45 | + const codeEl = pre.querySelector('code'); |
| 46 | + if (!codeEl) return; // 没有 code 标签就跳过 |
| 47 | + |
| 48 | + // 防止重复添加按钮 |
| 49 | + if (pre.querySelector('.custom-download-btn')) return; |
| 50 | + |
| 51 | + // 创建一个新的按钮元素 |
| 52 | + const btn = document.createElement('button'); |
| 53 | + btn.className = 'custom-download-btn'; |
| 54 | + btn.textContent = '下载'; // 按钮文字 |
| 55 | + |
| 56 | + // 按钮的样式 |
| 57 | + Object.assign(btn.style, { |
| 58 | + position: 'absolute', // 绝对定位,放在代码块右上角 |
| 59 | + top: '8px', // 距离上方 8px |
| 60 | + right: '80px', // 距离右侧 80px(避免和复制按钮重叠) |
| 61 | + zIndex: 20, // 层级比代码高 |
| 62 | + padding: '2px 6px', // 按钮内边距 |
| 63 | + fontSize: '12px', // 字体大小 |
| 64 | + borderRadius: '4px', // 圆角 |
| 65 | + border: 'none', // 去掉边框 |
| 66 | + background: 'rgba(0,0,0,0.6)', // 半透明背景 |
| 67 | + color: '#fff', // 白色文字 |
| 68 | + cursor: 'pointer', // 鼠标悬停显示小手 |
| 69 | + opacity: '0', // 默认隐藏(hover 时再显示) |
| 70 | + transition: 'opacity 0.2s ease, background 0.2s', // 平滑过渡动画 |
| 71 | + }); |
| 72 | + |
| 73 | + // 当鼠标移入代码块时显示按钮 |
| 74 | + pre.addEventListener('mouseenter', () => { |
| 75 | + btn.style.opacity = '1'; |
| 76 | + }); |
| 77 | + |
| 78 | + // 当鼠标移出代码块时隐藏按钮 |
| 79 | + pre.addEventListener('mouseleave', () => { |
| 80 | + btn.style.opacity = '0'; |
| 81 | + }); |
| 82 | + |
| 83 | + // 鼠标移入按钮时加深背景色 |
| 84 | + btn.onmouseover = () => { |
| 85 | + btn.style.background = 'rgba(0,0,0,0.8)'; |
| 86 | + }; |
| 87 | + |
| 88 | + // 鼠标移出按钮时恢复背景色 |
| 89 | + btn.onmouseout = () => { |
| 90 | + btn.style.background = 'rgba(0,0,0,0.6)'; |
| 91 | + }; |
| 92 | + |
| 93 | + // 点击下载按钮的逻辑 |
| 94 | + btn.onclick = (e) => { |
| 95 | + e.stopPropagation(); // 阻止事件冒泡,避免干扰其它组件 |
| 96 | + const codeText = codeEl.innerText; // 获取代码内容 |
| 97 | + |
| 98 | + // 提取代码块语言(如 language-js、language-python) |
| 99 | + let langClass = codeEl.className.match(/language-(\w+)/); |
| 100 | + if (!langClass || !langClass[1]) { |
| 101 | + langClass = pre.className.match(/language-(\w+)/); |
| 102 | + } |
| 103 | + const lang = langClass ? langClass[1].toLowerCase() : ''; |
| 104 | + |
| 105 | + // 语言后缀映射表,用于生成合适的文件名 |
| 106 | + const extMap = { |
| 107 | + javascript: 'js', js: 'js', |
| 108 | + typescript: 'ts', ts: 'ts', |
| 109 | + jsx: 'jsx', tsx: 'tsx', |
| 110 | + bash: 'sh', sh: 'sh', shell: 'sh', |
| 111 | + yaml: 'yaml', yml: 'yml', |
| 112 | + json: 'json', go: 'go', |
| 113 | + python: 'py', py: 'py', |
| 114 | + java: 'java', c: 'c', cpp: 'cpp', |
| 115 | + html: 'html', css: 'css', |
| 116 | + dockerfile: 'dockerfile', sql: 'sql', |
| 117 | + }; |
| 118 | + |
| 119 | + // 根据语言选择文件后缀(默认为 .txt) |
| 120 | + const ext = extMap[lang] || 'txt'; |
| 121 | + const filename = `code.${ext}`; // 下载的文件名 |
| 122 | + |
| 123 | + // 创建 Blob 对象(文本内容转为文件流) |
| 124 | + const blob = new Blob([codeText], { type: 'text/plain;charset=utf-8' }); |
| 125 | + |
| 126 | + // 动态创建 <a> 标签触发下载 |
| 127 | + const a = document.createElement('a'); |
| 128 | + a.href = URL.createObjectURL(blob); |
| 129 | + a.download = filename; |
| 130 | + document.body.appendChild(a); |
| 131 | + a.click(); // 模拟点击下载 |
| 132 | + a.remove(); // 下载完成后移除链接 |
| 133 | + }; |
| 134 | + |
| 135 | + // 把按钮插入到代码块中 |
| 136 | + pre.appendChild(btn); |
| 137 | + }); |
| 138 | + } |
| 139 | + |
| 140 | + // 初始执行一次 |
| 141 | + addDownloadButtons(); |
| 142 | + |
| 143 | + // 监听页面变化(Docusaurus 是单页应用,切换时需重新绑定) |
| 144 | + const observer = new MutationObserver(addDownloadButtons); |
| 145 | + observer.observe(document.body, { childList: true, subtree: true }); |
| 146 | + |
| 147 | + // 清理函数:卸载脚本 & 停止监听 |
| 148 | + return () => { |
| 149 | + try { document.body.removeChild(live2dScript); } catch {} |
| 150 | + observer.disconnect(); |
| 151 | + }; |
| 152 | + }, []); |
| 153 | +``` |
| 154 | +
|
| 155 | +
|
| 156 | +
|
| 157 | +
|
| 158 | +
|
| 159 | +效果如下 |
| 160 | +
|
| 161 | + |
0 commit comments