feat(C-03):代码在线运行+课程练一节
新增CodeRunner组件(JS worker沙箱+Python pyodide)与/utility/code-runner独立页;CourseLearn新增练习Tab(首代码块自动提取装载,无示例可直接练习)。
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
.code-runner-page {
|
||||
min-height: 100vh;
|
||||
background: #f8fafc;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.code-runner-page-inner {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.code-runner-page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.code-runner-page-desc {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CodeRunner from "@/components/CodeRunner/CodeRunner";
|
||||
import "./CodeRunnerPage.css";
|
||||
|
||||
const CodeRunnerPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="code-runner-page">
|
||||
<div className="code-runner-page-inner">
|
||||
<h1 className="code-runner-page-title">
|
||||
<CodeRunnerIcon /> {t("codeRunner.pageTitle")}
|
||||
</h1>
|
||||
<p className="code-runner-page-desc">{t("codeRunner.pageDesc")}</p>
|
||||
<CodeRunner height={520} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CodeRunnerIcon: React.FC = () => (
|
||||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" style={{ verticalAlign: "-4px", marginRight: 8 }}>
|
||||
<path d="M8 6L3 12L8 18" stroke="#667eea" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M16 6L21 12L16 18" stroke="#764ba2" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M13.5 4L10.5 20" stroke="#f59e0b" strokeWidth="2.4" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default CodeRunnerPage;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user