Skip to content

Commit c8e1b19

Browse files
qdrivenclaude
andcommitted
feat: implement UC1 - upload markdown/mdx files for executable tutorials
- Add SeriesLesson and LessonSection types for file-based tutorial management - Create CreateLessonPage with two-step flow: create lesson metadata then upload files - Create LessonPreviewPage with sequential section navigation and markdown rendering - Create LessonListPage for managing created lessons - Add markdown utility for file parsing and code block extraction - Update Zustand store with full CRUD actions for lessons and sections - Support section reordering with up/down buttons - Fix pre-existing TS errors in Terminal, TutorialCard, and TutorialDetail Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b96116e commit c8e1b19

File tree

12 files changed

+904
-28
lines changed

12 files changed

+904
-28
lines changed

apps/desktop/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@
1111
},
1212
"dependencies": {
1313
"@tauri-apps/api": "^2.10.1",
14+
"@tauri-apps/plugin-dialog": "^2.7.0",
15+
"@tauri-apps/plugin-fs": "^2.5.0",
1416
"@tauri-apps/plugin-opener": "^2",
1517
"lucide-react": "^1.7.0",
1618
"react": "^19.1.0",
1719
"react-dom": "^19.1.0",
20+
"react-markdown": "^10.1.0",
21+
"remark-gfm": "^4.0.1",
1822
"zustand": "^5.0.12"
1923
},
2024
"devDependencies": {

apps/desktop/src/App.tsx

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,59 @@ import { Terminal } from './components/terminal/Terminal';
44
import { Home } from './components/Home';
55
import { SeriesDetail } from './components/SeriesDetail';
66
import { TutorialDetail } from './components/TutorialDetail';
7+
import { CreateLessonPage } from './components/lesson/CreateLessonPage';
8+
import { LessonPreviewPage } from './components/lesson/LessonPreviewPage';
9+
import { LessonListPage } from './components/lesson/LessonListPage';
710
import { useAppStore } from './store/useAppStore';
811

912
type Tab = 'home' | 'tutorials' | 'series' | 'settings';
10-
type View =
13+
type View =
1114
| { type: 'home' }
1215
| { type: 'series'; seriesId: string }
13-
| { type: 'tutorial'; tutorialId: string };
16+
| { type: 'tutorial'; tutorialId: string }
17+
| { type: 'create-lesson' }
18+
| { type: 'lesson-list' }
19+
| { type: 'lesson-preview'; lessonId: string };
1420

1521
function App() {
1622
const [activeTab, setActiveTab] = useState<Tab>('home');
1723
const [currentView, setCurrentView] = useState<View>({ type: 'home' });
18-
const { terminalPosition, terminalVisible } = useAppStore();
19-
24+
const { terminalPosition, terminalVisible, setPreviewSectionIndex } = useAppStore();
25+
2026
const handleSeriesClick = (seriesId: string) => {
2127
setCurrentView({ type: 'series', seriesId });
2228
};
23-
29+
2430
const handleTutorialClick = (tutorialId: string) => {
2531
setCurrentView({ type: 'tutorial', tutorialId });
2632
};
27-
33+
2834
const handleBack = () => {
2935
setCurrentView({ type: 'home' });
3036
};
31-
37+
3238
const handleImportClick = () => {
33-
// TODO: Implement import functionality
34-
alert('导入功能开发中...');
39+
setCurrentView({ type: 'create-lesson' });
3540
};
36-
41+
3742
const handleAddDirectoryClick = () => {
38-
// TODO: Implement add directory functionality
39-
alert('添加目录功能开发中...');
43+
setCurrentView({ type: 'create-lesson' });
44+
};
45+
46+
const handleLessonCreated = (lessonId: string) => {
47+
setPreviewSectionIndex(0);
48+
setCurrentView({ type: 'lesson-preview', lessonId });
4049
};
41-
50+
51+
const handleTabChange = (tab: Tab) => {
52+
setActiveTab(tab);
53+
if (tab === 'series') {
54+
setCurrentView({ type: 'lesson-list' });
55+
} else if (tab === 'home') {
56+
setCurrentView({ type: 'home' });
57+
}
58+
};
59+
4260
const renderContent = () => {
4361
switch (currentView.type) {
4462
case 'home':
@@ -65,29 +83,54 @@ function App() {
6583
onBack={handleBack}
6684
/>
6785
);
86+
case 'create-lesson':
87+
return (
88+
<CreateLessonPage
89+
onBack={handleBack}
90+
onLessonCreated={handleLessonCreated}
91+
/>
92+
);
93+
case 'lesson-list':
94+
return (
95+
<LessonListPage
96+
onBack={handleBack}
97+
onCreateNew={() => setCurrentView({ type: 'create-lesson' })}
98+
onLessonClick={(id) => {
99+
setPreviewSectionIndex(0);
100+
setCurrentView({ type: 'lesson-preview', lessonId: id });
101+
}}
102+
/>
103+
);
104+
case 'lesson-preview':
105+
return (
106+
<LessonPreviewPage
107+
lessonId={currentView.lessonId}
108+
onBack={() => setCurrentView({ type: 'lesson-list' })}
109+
/>
110+
);
68111
default:
69112
return <div className="p-8 text-text-muted">页面开发中...</div>;
70113
}
71114
};
72-
115+
73116
return (
74117
<div className="h-screen w-screen flex flex-col bg-bg-primary text-text-primary overflow-hidden">
75118
{/* Header */}
76-
<Header activeTab={activeTab} onTabChange={setActiveTab} />
77-
119+
<Header activeTab={activeTab} onTabChange={handleTabChange} />
120+
78121
{/* Main Content Area */}
79122
<div className="flex-1 flex overflow-hidden">
80123
{/* Content */}
81124
<main className="flex-1 overflow-hidden">
82125
{renderContent()}
83126
</main>
84-
127+
85128
{/* Terminal - Right Side */}
86129
{terminalVisible && terminalPosition === 'right' && (
87130
<Terminal />
88131
)}
89132
</div>
90-
133+
91134
{/* Terminal - Bottom */}
92135
{terminalVisible && terminalPosition === 'bottom' && (
93136
<Terminal />

apps/desktop/src/components/TutorialDetail.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState } from 'react';
2-
import { ArrowLeft, Play, CheckCircle, Clock, BookOpen, Terminal, Copy, Check, Sparkles, RotateCcw } from 'lucide-react';
2+
import { ArrowLeft, Play, CheckCircle, Clock, Terminal, Copy, Check, Sparkles, RotateCcw } from 'lucide-react';
33
import { useAppStore } from '../store/useAppStore';
44

55
interface TutorialDetailProps {
@@ -30,7 +30,7 @@ export function TutorialDetail({ tutorialId, onBack }: TutorialDetailProps) {
3030
);
3131
}
3232

33-
const handleRun = (code: string, id: string) => {
33+
const handleRun = (code: string, _id: string) => {
3434
showTerminal();
3535
setIsExecuting(true);
3636

@@ -196,7 +196,7 @@ export function TutorialDetail({ tutorialId, onBack }: TutorialDetailProps) {
196196
<div
197197
className="text-text-secondary leading-relaxed"
198198
dangerouslySetInnerHTML={{
199-
__html: section.content
199+
__html: (section.content || "")
200200
.replace(/## (.*)/, '<h2 class="text-2xl font-bold text-text-primary mb-4 mt-8">$1</h2>')
201201
.replace(/- (.*)/g, '<li class="ml-4 mb-2">$1</li>')
202202
.replace(/`([^`]+)`/g, '<code class="px-1.5 py-0.5 bg-bg-card rounded text-accent text-sm">$1</code>')
@@ -218,14 +218,14 @@ export function TutorialDetail({ tutorialId, onBack }: TutorialDetailProps) {
218218
</div>
219219
<div className="flex items-center gap-2">
220220
<button
221-
onClick={() => handleCopy(section.code, section.id)}
221+
onClick={() => handleCopy(section.code!, section.id!)}
222222
className="p-2 text-text-muted hover:text-text-primary hover:bg-bg-tertiary rounded-lg transition-all"
223223
title="复制"
224224
>
225225
{copiedId === section.id ? <Check size={18} className="text-success" /> : <Copy size={18} />}
226226
</button>
227227
<button
228-
onClick={() => handleRun(section.code, section.id)}
228+
onClick={() => handleRun(section.code!, section.id!)}
229229
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-accent to-secondary text-white font-medium rounded-xl hover:shadow-glow transition-all"
230230
>
231231
<Play size={16} className="fill-current" />

0 commit comments

Comments
 (0)