Skip to content

Commit 961a0bb

Browse files
author
pptfz
committed
新增代码块下载按钮功能
1 parent ba04e62 commit 961a0bb

6 files changed

Lines changed: 1187 additions & 660 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
![iShot_2025-10-11_10.27.21](https://raw.githubusercontent.com/pptfz/picgo-images/master/img/iShot_2025-10-11_10.27.21.png)

docusaurus.config.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,12 @@ const config = {
5252
trailingSlash: false,
5353

5454
onBrokenLinks: 'throw',
55-
onBrokenMarkdownLinks: 'warn',
56-
55+
// onBrokenMarkdownLinks: 'warn',
56+
markdown: {
57+
hooks: {
58+
onBrokenMarkdownLinks: 'warn',
59+
},
60+
},
5761
// Even if you don't use internalization, you can use this field to set useful
5862
// metadata like html lang. For example, if your site is Chinese, you may want
5963
// to replace "en" with "zh-Hans".
@@ -255,7 +259,8 @@ const config = {
255259
// copyright: `Copyright © ${new Date().getFullYear()} 泡泡吐肥皂o <div id="site-uptime"></div>`,
256260
},
257261
prism: {
258-
additionalLanguages: ['Bash','nginx','Git','ini','JSON'],
262+
// https://prismjs.com/#supported-languages
263+
additionalLanguages: ['bash','nginx','git','ini','json','docker','yaml'],
259264
theme: prismThemes.dracula, // 亮色模式下的样式
260265
darkTheme: prismThemes.dracula, // 暗色模式下的样式
261266
},

0 commit comments

Comments
 (0)