23.内容拓展:代码运行器功能实现指南

23.内容拓展:代码运行器功能实现指南

本指南提供了在AnZhiYu主题中集成代码运行器功能的完整实现步骤,包括两级导航菜单、多服务商支持、响应式设计等功能。

步骤1:修改主题配置文件

文件路径: _config.anzhiyu.yml

在配置文件中添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 代码运行器配置
code_runner:
enable: true # 是否启用代码运行器功能
title: "代码运行器" # 面板标题
button_title: "代码运行器" # 按钮提示文字
panel_width: "600px" # 面板宽度
auto_load_first: false # 是否自动加载第一个实例
close_on_escape: true # 是否支持ESC键关闭
remember_selection: true # 是否记住用户选择

# 服务商分类配置
categories:
# 第一个分类:Trinket
- name: "Trinket"
icon: "fas fa-leaf"
description: "适合Python/HTML/CSS/JS等基础代码运行,界面简洁易用"
instances:
- name: "Python 3"
url: "https://trinket.io/embed/python3/f417f7026885"
description: "Python 3 在线编程环境"
- name: "HTML/CSS/JS"
url: "https://trinket.io/embed/html/1aac0e8640a7"
description: "前端三件套在线编辑器"
- name: "Java"
url: "https://trinket.io/embed/java/33cfa8ec292c"
description: "Java 在线编程环境"

# 第二个分类:JDoodle
- name: "JDoodle"
icon: "fas fa-terminal"
description: "支持70+种编程语言,功能强大的在线编译器"
instances:
- name: "C++ Compiler"
url: "https://www.jdoodle.com/online-compiler-c++/"
description: "C++ 在线编译器"
- name: "Java Compiler"
url: "https://www.jdoodle.com/online-java-compiler/"
description: "Java 在线编译器"
- name: "Python 3"
url: "https://www.jdoodle.com/python3-programming-online/"
description: "Python 3 在线编程"
- name: "Go Playground"
url: "https://www.jdoodle.com/compile-go-online/"
description: "Go 语言在线编程"

# 第三个分类:CodePen
- name: "CodePen"
icon: "fab fa-codepen"
description: "前端开发者的在线代码编辑器和社区"
instances:
- name: "HTML/CSS/JS"
url: "https://codepen.io/pen/"
description: "CodePen 在线编辑器"
- name: "React Playground"
url: "https://codepen.io/pen/?template=react"
description: "React 在线开发环境"

修改rightside按钮配置:

1
2
3
4
rightside_item_order: # 右下角按钮顺序和显示控制
enable: true # 是否启用自定义右下角按钮顺序
hide: readmode,translate,darkmode,hideAside # 要隐藏的按钮列表
show: toc,chat,comment,downloadMd,docToc,codeRunner # 要显示的按钮列表 (添加codeRunner)

修改inject配置:

1
2
3
4
5
6
7
8
9
10
inject:
head:
# 其他现有配置...
# 代码运行器样式
- '<link rel="stylesheet" href="/css/code-runner.css">'

bottom:
# 其他现有配置...
# 代码运行器功能脚本
- '<script src="/js/code-runner.js"></script>'

步骤2:修改rightside.pug文件

文件路径: themes/anzhiyu/layout/includes/rightside.pug

在rightside.pug文件的case语句中添加codeRunner分支:

1
2
3
4
when 'codeRunner'
if theme.code_runner && theme.code_runner.enable
button#code-runner-btn(type="button" title=theme.code_runner.button_title || "代码运行器")
i.fas.fa-code

步骤3:修改layout.pug文件

文件路径: themes/anzhiyu/layout/includes/layout.pug

在rightside.pug之后添加面板HTML结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//- 代码运行器面板
if theme.code_runner && theme.code_runner.enable
#code-runner-panel.code-runner-panel
.panel-header
.panel-title= theme.code_runner.title || "代码运行器"
button.panel-close-btn(type="button" title="关闭")
i.fas.fa-times

.panel-body
//- 左侧导航菜单
nav.panel-nav
each category in theme.code_runner.categories
.nav-category(data-category=category.name)
.category-header(
data-description=category.description
title=category.description
)
if category.icon
i(class=category.icon)
span.category-name= category.name
i.expand-icon.fas.fa-chevron-down

ul.instance-list
each instance in category.instances
li.instance-item
a.instance-link(
href="javascript:void(0);"
data-url=instance.url
data-name=instance.name
title=instance.description || instance.name
)= instance.name

//- 右侧内容区
.panel-content
.welcome-message
.welcome-icon
i.fas.fa-code
.welcome-text
h3 欢迎使用代码运行器
p 请从左侧菜单选择一个编程环境开始编码

.iframe-container
iframe#code-runner-iframe(
frameborder="0"
width="100%"
height="100%"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
)

.loading-indicator
.loading-spinner
i.fas.fa-spinner.fa-spin
.loading-text 正在加载编程环境...

步骤4:修改config.pug文件

文件路径: themes/anzhiyu/layout/includes/head/config.pug

在GLOBAL_CONFIG对象中添加code_runner配置:

1
code_runner: !{theme.code_runner ? JSON.stringify(theme.code_runner) : 'null'}

将此行添加到GLOBAL_CONFIG对象的其他配置项中。

步骤5:创建CSS样式文件

文件路径: themes/anzhiyu/source/css/code-runner.css

创建完整的CSS文件(内容较长,见下一部分)。

步骤6:创建JavaScript功能文件

文件路径: themes/anzhiyu/source/js/code-runner.js

创建完整的JavaScript文件(内容较长,见下一部分)。

完整代码文件

CSS样式文件内容

文件: themes/anzhiyu/source/css/code-runner.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
/* 代码运行器面板样式 */
#code-runner-panel {
position: fixed;
top: 0;
right: 0;
width: 600px;
height: 100vh;
background: var(--anzhiyu-card-bg);
backdrop-filter: blur(20px);
border-left: var(--style-border-always);
box-shadow: var(--anzhiyu-shadow-lightblack);
z-index: 1001;
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
overflow: hidden;
}

#code-runner-panel.active {
transform: translateX(0);
}

/* 面板头部 */
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-bottom: var(--style-border-always);
background: var(--anzhiyu-card-bg);
backdrop-filter: blur(20px);
position: relative;
z-index: 1;
}

.panel-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--anzhiyu-fontcolor);
margin: 0;
}

.panel-close-btn {
width: 2rem;
height: 2rem;
border: none;
background: transparent;
color: var(--anzhiyu-fontcolor);
cursor: pointer;
border-radius: var(--anzhiyu-border-radius);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}

.panel-close-btn:hover {
background: var(--anzhiyu-secondbg);
color: var(--anzhiyu-red);
}

/* 面板主体 */
.panel-body {
flex: 1;
display: flex;
overflow: hidden;
}

/* 左侧导航 */
.panel-nav {
width: 200px;
flex-shrink: 0;
border-right: var(--style-border-always);
padding: 1rem;
overflow-y: auto;
background: var(--anzhiyu-card-bg);
}

.nav-category {
margin-bottom: 0.5rem;
}

.category-header {
display: flex;
align-items: center;
padding: 0.75rem;
cursor: pointer;
border-radius: var(--anzhiyu-border-radius);
transition: all 0.3s ease;
position: relative;
user-select: none;
}

.category-header:hover {
background: var(--anzhiyu-secondbg);
}

.category-header i:first-child {
margin-right: 0.5rem;
color: var(--anzhiyu-main);
}

.category-name {
flex: 1;
font-weight: 500;
color: var(--anzhiyu-fontcolor);
}

.expand-icon {
margin-left: 0.5rem;
transition: transform 0.3s ease;
color: var(--anzhiyu-fontcolor);
opacity: 0.7;
}

.nav-category.expanded .expand-icon {
transform: rotate(180deg);
}

/* 实例列表 */
.instance-list {
list-style: none;
padding: 0;
margin: 0;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}

.nav-category.expanded .instance-list {
max-height: 300px;
}

.instance-item {
margin: 0;
}

.instance-link {
display: block;
padding: 0.5rem 0.75rem;
margin-left: 1.5rem;
color: var(--anzhiyu-fontcolor);
text-decoration: none;
border-radius: var(--anzhiyu-border-radius);
transition: all 0.3s ease;
font-size: 0.9rem;
border-left: 2px solid transparent;
}

.instance-link:hover {
background: var(--anzhiyu-secondbg);
color: var(--anzhiyu-main);
border-left-color: var(--anzhiyu-main);
}

.instance-link.active {
background: var(--anzhiyu-main);
color: var(--anzhiyu-white);
font-weight: 500;
}

/* 右侧内容区 */
.panel-content {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
}

.welcome-message {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
color: var(--anzhiyu-fontcolor);
}

.welcome-icon i {
font-size: 3rem;
color: var(--anzhiyu-main);
margin-bottom: 1rem;
}

.welcome-text h3 {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
font-weight: 600;
}

.welcome-text p {
margin: 0;
opacity: 0.7;
font-size: 0.9rem;
}

.iframe-container {
flex: 1;
position: relative;
display: none;
}

.iframe-container.active {
display: block;
}

.iframe-container iframe {
width: 100%;
height: 100%;
border: none;
background: var(--anzhiyu-white);
}

.loading-indicator {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--anzhiyu-card-bg);
color: var(--anzhiyu-fontcolor);
z-index: 2;
}

.loading-indicator.active {
display: flex;
}

.loading-spinner i {
font-size: 2rem;
color: var(--anzhiyu-main);
margin-bottom: 1rem;
}

.loading-text {
font-size: 0.9rem;
opacity: 0.8;
}

/* 全局状态 */
body.code-runner-open {
overflow: hidden;
}

/* 响应式设计 */
@media (max-width: 768px) {
#code-runner-panel {
width: 100vw;
transform: translateX(100%);
}

#code-runner-panel.active {
transform: translateX(0);
}

.panel-nav {
width: 150px;
}

.panel-header {
padding: 0.75rem 1rem;
}

.panel-title {
font-size: 1rem;
}
}

@media (max-width: 480px) {
.panel-nav {
width: 120px;
}

.category-header {
padding: 0.5rem;
}

.category-name {
font-size: 0.85rem;
}

.instance-link {
font-size: 0.8rem;
padding: 0.4rem 0.5rem;
}
}

/* 暗色模式适配 */
[data-theme="dark"] #code-runner-panel .iframe-container iframe {
background: var(--anzhiyu-card-bg);
}

/* 按钮激活状态 */
#code-runner-btn.active {
background-color: var(--anzhiyu-main);
color: var(--anzhiyu-white);
}

/* Tooltip样式 */
.category-header::after {
content: attr(data-description);
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
margin-left: 10px;
background: var(--anzhiyu-card-bg);
color: var(--anzhiyu-fontcolor);
padding: 0.5rem 0.75rem;
border-radius: var(--anzhiyu-border-radius);
font-size: 0.8rem;
white-space: nowrap;
box-shadow: var(--anzhiyu-shadow-lightblack);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 1002;
border: var(--style-border-always);
}

.category-header:hover::after {
opacity: 1;
visibility: visible;
}
JavaScript功能文件内容

文件: themes/anzhiyu/source/js/code-runner.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
/**
* 代码运行器功能
* Code Runner functionality for AnZhiYu theme
* 基于doc-sidebar的实现模式
*/

// 初始化函数,支持PJAX
function initCodeRunner() {
const codeRunnerPanel = document.getElementById('code-runner-panel');
const codeRunnerButton = document.getElementById('code-runner-btn');

if (!codeRunnerPanel || !codeRunnerButton) {
console.log('Code Runner: Panel or button not found');
return;
}

console.log('Code Runner: Initializing...');

// 初始化代码运行器功能
initCodeRunnerPanel();

function initCodeRunnerPanel() {
// 设置初始状态
setupInitialState();

// 设置按钮功能
setupToggleButton();

// 设置面板内部交互
setupPanelInteractions();

// 恢复用户选择
restoreUserSelection();
}

// 设置初始状态
function setupInitialState() {
// 确保面板初始状态为隐藏
codeRunnerPanel.classList.remove('active');
document.body.classList.remove('code-runner-open');

// 设置iframe和加载指示器初始状态
const iframeContainer = codeRunnerPanel.querySelector('.iframe-container');
const loadingIndicator = codeRunnerPanel.querySelector('.loading-indicator');
const welcomeMessage = codeRunnerPanel.querySelector('.welcome-message');
const iframe = codeRunnerPanel.querySelector('#code-runner-iframe');

if (iframeContainer) iframeContainer.classList.remove('active');
if (loadingIndicator) loadingIndicator.classList.remove('active');
if (welcomeMessage) welcomeMessage.style.display = 'flex';
if (iframe) iframe.src = '';
}

// 设置切换按钮功能
function setupToggleButton() {
codeRunnerButton.addEventListener('click', () => {
const isOpen = codeRunnerPanel.classList.contains('active');

if (isOpen) {
closePanel();
} else {
openPanel();
}
});
}

// 打开面板
function openPanel() {
codeRunnerPanel.classList.add('active');
document.body.classList.add('code-runner-open');
codeRunnerButton.classList.add('active');

console.log('Code Runner: Panel opened');

// 延迟自动加载第一个实例(如果配置允许)
const config = window.GLOBAL_CONFIG?.code_runner || {};
if (config.auto_load_first !== false) {
// 延迟加载,避免PJAX切换时立即加载导致失败
setTimeout(() => {
autoLoadFirstInstance();
}, 1000); // 延迟1秒
}
}

// 关闭面板
function closePanel() {
codeRunnerPanel.classList.remove('active');
document.body.classList.remove('code-runner-open');
codeRunnerButton.classList.remove('active');

console.log('Code Runner: Panel closed');
}

// 设置面板内部交互
function setupPanelInteractions() {
// 关闭按钮
const closeBtn = codeRunnerPanel.querySelector('.panel-close-btn');
if (closeBtn) {
closeBtn.addEventListener('click', closePanel);
}

// 分类展开/收缩事件
const categoryHeaders = codeRunnerPanel.querySelectorAll('.category-header');
categoryHeaders.forEach(header => {
header.addEventListener('click', () => {
const category = header.closest('.nav-category');
toggleCategory(category);
});
});

// 实例选择事件
const instanceLinks = codeRunnerPanel.querySelectorAll('.instance-link');
instanceLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
selectInstance(link);
});
});

// ESC键关闭
const config = window.GLOBAL_CONFIG?.code_runner || {};
if (config.close_on_escape !== false) {
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && codeRunnerPanel.classList.contains('active')) {
closePanel();
}
});
}

// 点击面板外部关闭
document.addEventListener('click', (e) => {
if (codeRunnerPanel.classList.contains('active') &&
!codeRunnerPanel.contains(e.target) &&
!codeRunnerButton.contains(e.target)) {
closePanel();
}
});
}

// 切换分类展开/收缩
function toggleCategory(category) {
if (!category) return;

// 收缩所有其他分类
const allCategories = codeRunnerPanel.querySelectorAll('.nav-category');
allCategories.forEach(cat => {
if (cat !== category) {
cat.classList.remove('expanded');
}
});

// 切换当前分类
category.classList.toggle('expanded');

console.log('Code Runner: Category toggled', category.dataset.category);
}

// 选择实例
function selectInstance(link) {
const url = link.dataset.url;
const name = link.dataset.name;

if (!url) {
console.warn('Code Runner: No URL found for instance', name);
return;
}

// 更新选中状态
const allLinks = codeRunnerPanel.querySelectorAll('.instance-link');
allLinks.forEach(l => l.classList.remove('active'));
link.classList.add('active');

// 加载iframe
loadIframe(url);

console.log('Code Runner: Instance selected', name, url);
}

// 加载iframe
function loadIframe(url, retryCount = 0) {
const iframe = codeRunnerPanel.querySelector('#code-runner-iframe');
const loadingIndicator = codeRunnerPanel.querySelector('.loading-indicator');
const welcomeMessage = codeRunnerPanel.querySelector('.welcome-message');
const iframeContainer = codeRunnerPanel.querySelector('.iframe-container');

if (!iframe) {
console.error('Code Runner: iframe not found');
return;
}

// 显示加载指示器
if (welcomeMessage) welcomeMessage.style.display = 'none';
if (iframeContainer) iframeContainer.classList.remove('active');
if (loadingIndicator) loadingIndicator.classList.add('active');

// 清除之前的事件监听器
iframe.onload = null;
iframe.onerror = null;

// 设置加载超时
const loadingTimeout = setTimeout(() => {
onIframeError();
}, 20000); // 增加到20秒超时

// iframe加载完成处理
const onIframeLoad = () => {
clearTimeout(loadingTimeout);
if (loadingIndicator) loadingIndicator.classList.remove('active');
if (iframeContainer) iframeContainer.classList.add('active');
console.log('Code Runner: iframe loaded successfully');
};

// iframe加载错误处理
const onIframeError = () => {
clearTimeout(loadingTimeout);

// 如果重试次数少于2次,则重试
if (retryCount < 2) {
console.warn(`Code Runner: iframe load failed, retrying... (${retryCount + 1}/2)`);
setTimeout(() => {
loadIframe(url, retryCount + 1);
}, 2000); // 延迟2秒重试
return;
}

// 重试失败,显示错误信息
if (loadingIndicator) loadingIndicator.classList.remove('active');
if (welcomeMessage) {
welcomeMessage.style.display = 'flex';
const welcomeText = welcomeMessage.querySelector('.welcome-text');
if (welcomeText) {
welcomeText.innerHTML = `
<h3>加载失败</h3>
<p>无法加载编程环境,请检查网络连接或尝试其他选项</p>
<button onclick="location.reload()" style="margin-top: 10px; padding: 5px 10px; background: var(--anzhiyu-main); color: white; border: none; border-radius: 4px; cursor: pointer;">刷新页面重试</button>
`;
}
}
console.error('Code Runner: iframe failed to load after retries', url);
};

// 绑定事件
iframe.onload = onIframeLoad;
iframe.onerror = onIframeError;

// 延迟设置iframe源,避免过快加载
setTimeout(() => {
iframe.src = url;
}, 500);
}

// 自动加载第一个实例
function autoLoadFirstInstance() {
const firstCategory = codeRunnerPanel.querySelector('.nav-category');
const firstInstance = firstCategory?.querySelector('.instance-link');

if (firstCategory && firstInstance) {
// 展开第一个分类
firstCategory.classList.add('expanded');

// 选择第一个实例
setTimeout(() => {
firstInstance.click();
}, 300); // 等待展开动画完成
}
}

// 恢复用户选择
function restoreUserSelection() {
const config = window.GLOBAL_CONFIG?.code_runner || {};
if (config.remember_selection === false) return;

try {
const saved = localStorage.getItem('code-runner-selection');
if (!saved) return;

const selection = JSON.parse(saved);
if (!selection.category || !selection.instance) return;

// 查找并恢复分类
const category = codeRunnerPanel.querySelector(`[data-category="${selection.category}"]`);
if (category) {
category.classList.add('expanded');

// 查找并恢复实例
const instance = category.querySelector(`[data-name="${selection.instance}"]`);
if (instance) {
instance.classList.add('active');
}
}
} catch (error) {
console.warn('Code Runner: Failed to restore selection', error);
}
}
}

// 防止重复初始化的标志
let codeRunnerInitialized = false;

// 页面加载完成时初始化
document.addEventListener('DOMContentLoaded', () => {
if (!codeRunnerInitialized) {
initCodeRunner();
codeRunnerInitialized = true;
}
});

// 为 PJAX 提供支持
document.addEventListener('pjax:complete', () => {
// 重置初始化标志,因为DOM可能已经改变
codeRunnerInitialized = false;

// 延迟执行以确保 DOM 完全加载
setTimeout(() => {
if (!codeRunnerInitialized) {
initCodeRunner();
codeRunnerInitialized = true;
}
}, 500); // 增加延迟时间
});