Skip to content

Commit b03c350

Browse files
committed
add executable tutorial
1 parent e8cc7a6 commit b03c350

File tree

11 files changed

+780
-696
lines changed

11 files changed

+780
-696
lines changed

playground/apps/desktop/next.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { NextConfig } from "next";
22

33
const nextConfig: NextConfig = {
44
output: "export",
5+
distDir: "out",
56
images: {
67
unoptimized: true,
78
},

playground/apps/desktop/src-tauri/tauri.conf.json

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,7 @@
2525
},
2626
"plugins": {
2727
"shell": {
28-
"scope": [
29-
{
30-
"name": "sh",
31-
"cmd": "sh",
32-
"args": true
33-
}
34-
]
28+
"open": "^"
3529
}
3630
},
3731
"bundle": {

playground/apps/desktop/src/app/page.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { useEffect, useState } from "react";
34
import { useAppStore } from "@/store/useAppStore";
45
import { useRouter } from "next/navigation";
56
import {
@@ -30,8 +31,24 @@ import {
3031

3132
export default function Home() {
3233
const router = useRouter();
34+
const [mounted, setMounted] = useState(false);
3335
const { series, getFilteredTutorials, progress, searchQuery, setSearchQuery } = useAppStore();
3436

37+
useEffect(() => {
38+
setMounted(true);
39+
}, []);
40+
41+
if (!mounted) {
42+
return (
43+
<div className="flex h-full items-center justify-center">
44+
<div className="flex items-center gap-3">
45+
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
46+
<span className="text-muted-foreground">加载中...</span>
47+
</div>
48+
</div>
49+
);
50+
}
51+
3552
const filteredTutorials = getFilteredTutorials();
3653
const recentTutorials = filteredTutorials.slice(0, 6);
3754

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
"use client";
2+
3+
import { useRouter } from "next/navigation";
4+
import { useAppStore } from "@/store/useAppStore";
5+
import {
6+
Card,
7+
CardContent,
8+
CardHeader,
9+
CardTitle,
10+
Button,
11+
Badge,
12+
} from "@innate/ui";
13+
import {
14+
ArrowLeft,
15+
BookOpen,
16+
Clock,
17+
BarChart3,
18+
Trophy,
19+
Play,
20+
Sparkles,
21+
CheckCircle,
22+
Circle,
23+
} from "lucide-react";
24+
25+
interface SeriesDetailClientProps {
26+
id: string;
27+
}
28+
29+
export default function SeriesDetailClient({ id }: SeriesDetailClientProps) {
30+
const router = useRouter();
31+
const seriesId = id;
32+
33+
const { series, getTutorialsBySeries, progress } = useAppStore();
34+
35+
const currentSeries = series.find((s) => s.id === seriesId);
36+
const tutorials = getTutorialsBySeries(seriesId);
37+
38+
if (!currentSeries) {
39+
return (
40+
<div className="flex items-center justify-center h-full">
41+
<div className="text-muted-foreground">系列不存在</div>
42+
</div>
43+
);
44+
}
45+
46+
const completedCount = tutorials.filter((t) => progress[t.id]?.completed).length;
47+
const progressPercent = tutorials.length > 0 ? (completedCount / tutorials.length) * 100 : 0;
48+
const totalDuration = tutorials.reduce((sum, t) => sum + t.duration, 0);
49+
const nextTutorial = tutorials.find((t) => !progress[t.id]?.completed);
50+
51+
const getDifficultyColor = (difficulty: string) => {
52+
switch (difficulty) {
53+
case "beginner":
54+
return "from-emerald-500 to-teal-500";
55+
case "intermediate":
56+
return "from-amber-500 to-orange-500";
57+
case "advanced":
58+
return "from-rose-500 to-pink-500";
59+
default:
60+
return "from-primary to-secondary";
61+
}
62+
};
63+
64+
const getDifficultyText = (difficulty: string) => {
65+
switch (difficulty) {
66+
case "beginner":
67+
return "入门";
68+
case "intermediate":
69+
return "进阶";
70+
case "advanced":
71+
return "高级";
72+
default:
73+
return "入门";
74+
}
75+
};
76+
77+
return (
78+
<div className="flex flex-col h-full overflow-auto">
79+
{/* Hero Header */}
80+
<div className="relative overflow-hidden">
81+
<div
82+
className="absolute inset-0 opacity-20"
83+
style={{
84+
background: `linear-gradient(135deg, ${currentSeries.color}40 0%, transparent 60%)`,
85+
}}
86+
/>
87+
<div
88+
className="absolute top-0 right-0 w-96 h-96 rounded-full blur-3xl -translate-y-1/2 translate-x-1/4"
89+
style={{ background: `${currentSeries.color}30` }}
90+
/>
91+
92+
<div className="relative px-8 py-8">
93+
<Button
94+
variant="ghost"
95+
onClick={() => router.push("/")}
96+
className="mb-4"
97+
>
98+
<ArrowLeft className="mr-2" size={16} />
99+
返回系列列表
100+
</Button>
101+
102+
<div className="flex flex-col md:flex-row md:items-start gap-6">
103+
<div
104+
className="w-20 h-20 rounded-2xl flex items-center justify-center text-4xl shrink-0"
105+
style={{
106+
background: `linear-gradient(135deg, ${currentSeries.color}30 0%, ${currentSeries.color}50 100%)`,
107+
}}
108+
>
109+
{currentSeries.icon || "📚"}
110+
</div>
111+
112+
<div className="flex-1">
113+
<div className="flex flex-wrap items-center gap-3 mb-3">
114+
<Badge
115+
className={`text-white bg-gradient-to-r ${getDifficultyColor(
116+
currentSeries.difficulty
117+
)}`}
118+
>
119+
{getDifficultyText(currentSeries.difficulty)}
120+
</Badge>
121+
<span className="flex items-center gap-1.5 text-sm text-muted-foreground">
122+
<BookOpen size={14} />
123+
{tutorials.length} 个教程
124+
</span>
125+
<span className="flex items-center gap-1.5 text-sm text-muted-foreground">
126+
<Clock size={14} />
127+
{totalDuration} 分钟
128+
</span>
129+
</div>
130+
131+
<h1 className="text-3xl font-bold mb-2">{currentSeries.title}</h1>
132+
<p className="text-muted-foreground text-lg max-w-2xl">
133+
{currentSeries.description}
134+
</p>
135+
136+
{/* Progress Section */}
137+
<div className="mt-6 p-4 bg-card/50 backdrop-blur-sm border rounded-xl">
138+
<div className="flex items-center justify-between mb-3">
139+
<div className="flex items-center gap-2">
140+
<BarChart3 className="text-primary" size={18} />
141+
<span className="font-medium">学习进度</span>
142+
</div>
143+
<div className="flex items-center gap-2">
144+
<span className="text-2xl font-bold text-primary">{Math.round(progressPercent)}%</span>
145+
<span className="text-muted-foreground">
146+
({completedCount}/{tutorials.length})
147+
</span>
148+
</div>
149+
</div>
150+
<div className="h-3 bg-muted rounded-full overflow-hidden">
151+
<div
152+
className="h-full bg-gradient-to-r from-primary to-secondary rounded-full transition-all"
153+
style={{ width: `${progressPercent}%` }}
154+
/>
155+
</div>
156+
157+
{nextTutorial && progressPercent < 100 && (
158+
<Button
159+
onClick={() => router.push(`/tutorial/${nextTutorial.id}`)}
160+
className="mt-4 bg-gradient-to-r from-primary to-secondary"
161+
>
162+
<Play className="mr-2 fill-current" size={16} />
163+
继续学习: {nextTutorial.title}
164+
</Button>
165+
)}
166+
167+
{progressPercent === 100 && (
168+
<div className="mt-4 flex items-center gap-2 px-4 py-2 bg-emerald-500/10 border border-emerald-500/30 text-emerald-500 rounded-xl">
169+
<Trophy size={18} />
170+
<span className="font-medium">恭喜!你已完成本系列所有教程</span>
171+
</div>
172+
)}
173+
</div>
174+
</div>
175+
</div>
176+
</div>
177+
</div>
178+
179+
{/* Tutorials List */}
180+
<div className="px-8 pb-8">
181+
<div className="flex items-center gap-3 mb-6">
182+
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-secondary flex items-center justify-center">
183+
<Sparkles className="text-primary-foreground" size={20} />
184+
</div>
185+
<div>
186+
<h2 className="text-xl font-bold">教程列表</h2>
187+
<p className="text-sm text-muted-foreground">按顺序完成所有教程</p>
188+
</div>
189+
</div>
190+
191+
{tutorials.length > 0 ? (
192+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
193+
{tutorials.map((tutorial) => {
194+
const isCompleted = progress[tutorial.id]?.completed;
195+
return (
196+
<Card
197+
key={tutorial.id}
198+
className="group cursor-pointer transition-all hover:shadow-lg hover:border-primary/50"
199+
onClick={() => router.push(`/tutorial/${tutorial.id}`)}
200+
>
201+
<CardHeader className="pb-2">
202+
<div className="flex items-start justify-between gap-3">
203+
<div className="flex-1 min-w-0">
204+
<div className="flex items-center gap-2 mb-1">
205+
<Badge
206+
className={`text-xs ${
207+
tutorial.difficulty === "beginner"
208+
? "bg-emerald-500/10 text-emerald-500"
209+
: tutorial.difficulty === "intermediate"
210+
? "bg-amber-500/10 text-amber-500"
211+
: "bg-rose-500/10 text-rose-500"
212+
}`}
213+
>
214+
{getDifficultyText(tutorial.difficulty)}
215+
</Badge>
216+
{isCompleted && (
217+
<Badge
218+
variant="outline"
219+
className="text-xs text-emerald-500 border-emerald-500/20"
220+
>
221+
<CheckCircle className="w-3 h-3 mr-1" />
222+
已完成
223+
</Badge>
224+
)}
225+
</div>
226+
<CardTitle className="text-base group-hover:text-primary transition-colors">
227+
{tutorial.title}
228+
</CardTitle>
229+
</div>
230+
<div
231+
className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${
232+
isCompleted
233+
? "bg-emerald-500/10 text-emerald-500"
234+
: "bg-muted text-muted-foreground"
235+
}`}
236+
>
237+
{isCompleted ? <CheckCircle size={18} /> : <Circle size={18} />}
238+
</div>
239+
</div>
240+
</CardHeader>
241+
<CardContent>
242+
<p className="text-sm text-muted-foreground line-clamp-2 mb-3">
243+
{tutorial.description}
244+
</p>
245+
<div className="flex items-center justify-between">
246+
<div className="flex items-center gap-3 text-sm text-muted-foreground">
247+
<div className="flex items-center gap-1">
248+
<Clock size={14} />
249+
<span>{tutorial.duration} 分钟</span>
250+
</div>
251+
</div>
252+
<Button
253+
size="sm"
254+
variant={isCompleted ? "outline" : "default"}
255+
className={
256+
isCompleted
257+
? "text-emerald-500 border-emerald-500/20"
258+
: "bg-gradient-to-r from-primary to-secondary opacity-0 group-hover:opacity-100 transition-opacity"
259+
}
260+
>
261+
<Play className="w-3 h-3 mr-1 fill-current" />
262+
{isCompleted ? "复习" : "开始"}
263+
</Button>
264+
</div>
265+
</CardContent>
266+
</Card>
267+
);
268+
})}
269+
</div>
270+
) : (
271+
<div className="text-center py-16 bg-card rounded-2xl border border-dashed">
272+
<BookOpen size={64} className="mx-auto mb-4 text-muted-foreground opacity-30" />
273+
<p className="text-muted-foreground text-lg">该系列暂无教程</p>
274+
</div>
275+
)}
276+
</div>
277+
</div>
278+
);
279+
}

0 commit comments

Comments
 (0)