feat(C-03):代码在线运行+课程练一节

新增CodeRunner组件(JS worker沙箱+Python pyodide)与/utility/code-runner独立页;CourseLearn新增练习Tab(首代码块自动提取装载,无示例可直接练习)。
This commit is contained in:
chunyu
2026-09-15 15:26:44 +08:00
parent abea2caa87
commit e90b787730
8 changed files with 562 additions and 1 deletions
+20
View File
@@ -529,6 +529,26 @@
font-size: 14px;
}
.practice-snippet-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 14px;
margin-bottom: 12px;
font-size: 13px;
background: rgba(102, 126, 234, 0.08);
border: 1px solid rgba(102, 126, 234, 0.25);
border-radius: 10px;
}
.practice-snippet-bar--empty {
justify-content: center;
color: var(--text-secondary);
background: transparent;
border-style: dashed;
}
.empty-video {
text-align: center;
padding: 60px 20px;
+40 -1
View File
@@ -22,6 +22,7 @@ import { useNavigate, useSearchParams } from "react-router-dom";
import { api_request } from "@/utils/request";
import { useRecordHistory } from "@/hooks/useRecordHistory";
import { BDCloudVideoView } from "@/components/BDCloudVideoView";
import CodeRunner, { extractFirstCodeBlock } from "@/components/CodeRunner/CodeRunner";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
@@ -84,6 +85,7 @@ const CourseLearn: React.FC = () => {
const [chapters, setChapters] = useState<ChapterData[]>([]);
const [currentChapterIndex, setCurrentChapterIndex] = useState(0);
const [chapterContent, setChapterContent] = useState("");
const [chapterSnippet, setChapterSnippet] = useState<{ lang: "javascript" | "python"; code: string } | null>(null);
const [completedChapters, setCompletedChapters] = useState<number[]>([]);
const [sidebarOpen, setSidebarOpen] = useState(false);
const [loading, setLoading] = useState(true);
@@ -161,15 +163,19 @@ const CourseLearn: React.FC = () => {
if (response.ok) {
const md = await response.text();
setChapterContent(md);
setChapterSnippet(extractFirstCodeBlock(md));
return;
}
} catch {}
try {
const res = await api_request.learn.get_chapter_content(chapterId);
const data = (res as any)?.data;
setChapterContent(data?.content_md || "");
const md = data?.content_md || "";
setChapterContent(md);
setChapterSnippet(extractFirstCodeBlock(md));
} catch {
setChapterContent("");
setChapterSnippet(null);
}
};
@@ -452,6 +458,39 @@ const CourseLearn: React.FC = () => {
</div>
),
},
{
key: "practice",
label: (
<span>
<PlayCircleOutlined /> {t('codeRunner.practice')}
</span>
),
children: (
<div>
{chapterSnippet ? (
<div className="practice-snippet-bar">
<span>本章检测到可运行代码示例({chapterSnippet.lang})</span>
<Button
size="small"
type="primary"
onClick={() => setChapterSnippet({ ...chapterSnippet })}
>
装载本章代码
</Button>
</div>
) : (
<div className="practice-snippet-bar practice-snippet-bar--empty">
本章暂无代码示例,可直接在下方练习
</div>
)}
<CodeRunner
key={`${currentChapter?.id}-${chapterSnippet?.code?.length ?? 0}`}
height={420}
initialCode={chapterSnippet?.code}
/>
</div>
),
},
]}
/>
</div>