's'
This commit is contained in:
151
app/components/content/LessonLayout.tsx
Normal file
151
app/components/content/LessonLayout.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { CourseConfig } from "@/lib/content/config";
|
||||
|
||||
type LessonLayoutProps = {
|
||||
courseConfig: CourseConfig;
|
||||
moduleIndex: number;
|
||||
lessonIndex: number;
|
||||
lessonName: string;
|
||||
lessonDuration: string;
|
||||
isFreeTrial: boolean;
|
||||
unlocked: boolean;
|
||||
totalLessons: number;
|
||||
completedCount: number;
|
||||
prevLesson: { moduleIndex: number; lessonIndex: number } | null;
|
||||
nextLesson: { moduleIndex: number; lessonIndex: number } | null;
|
||||
courseHomeHref: string;
|
||||
payHref: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function LessonLayout({
|
||||
courseConfig,
|
||||
moduleIndex,
|
||||
lessonIndex,
|
||||
lessonName,
|
||||
lessonDuration,
|
||||
isFreeTrial,
|
||||
unlocked,
|
||||
totalLessons,
|
||||
completedCount,
|
||||
prevLesson,
|
||||
nextLesson,
|
||||
courseHomeHref,
|
||||
payHref,
|
||||
children,
|
||||
}: LessonLayoutProps) {
|
||||
const router = useRouter();
|
||||
const courseBase = courseHomeHref.replace(/\/$/, "");
|
||||
const lessonBase = courseBase.includes("/course") ? courseBase : `${courseBase}/course`;
|
||||
const progressPercent = totalLessons > 0 ? Math.round((completedCount / totalLessons) * 100) : 0;
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.target && (e.target as HTMLElement).tagName === "INPUT") return;
|
||||
if (e.key === "ArrowLeft" && prevLesson) {
|
||||
router.push(`${lessonBase}/${prevLesson.moduleIndex}/${prevLesson.lessonIndex}`);
|
||||
} else if (e.key === "ArrowRight" && nextLesson) {
|
||||
router.push(`${lessonBase}/${nextLesson.moduleIndex}/${nextLesson.lessonIndex}`);
|
||||
}
|
||||
},
|
||||
[prevLesson, nextLesson, lessonBase, router]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--background)] text-[var(--foreground)]">
|
||||
<main className="pt-14 pb-16 md:pt-16">
|
||||
<div className="mx-auto max-w-4xl px-4 sm:px-6">
|
||||
{/* 进度条 */}
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between text-xs text-[var(--muted-foreground)] mb-1">
|
||||
<span>学习进度</span>
|
||||
<span>{completedCount}/{totalLessons} 节 · {progressPercent}%</span>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-[var(--muted)]">
|
||||
<div
|
||||
className="h-full rounded-full bg-[var(--accent)] transition-all duration-300"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 导航 */}
|
||||
<nav className="mb-6 flex items-center gap-2 text-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
className="flex items-center gap-1 text-[var(--muted-foreground)] transition hover:text-[var(--foreground)]"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
返回
|
||||
</button>
|
||||
<span className="text-[var(--muted-foreground)]">/</span>
|
||||
<Link href={courseHomeHref} className="text-[var(--muted-foreground)] transition hover:text-[var(--foreground)]">
|
||||
课程首页
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
{/* 标题 */}
|
||||
<h1 className="mb-2 flex items-center gap-2 text-xl font-bold sm:text-2xl md:text-3xl">
|
||||
{lessonName}
|
||||
{isFreeTrial && (
|
||||
<span className="rounded bg-[var(--accent)]/20 px-2 py-0.5 text-sm font-normal text-[var(--accent)]">
|
||||
试看
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
<p className="mb-8 text-sm text-[var(--muted-foreground)]">时长 {lessonDuration}</p>
|
||||
|
||||
{/* 内容区 */}
|
||||
{children}
|
||||
|
||||
{/* 上/下一节 */}
|
||||
<div className="mt-12 flex flex-col gap-3 sm:flex-row sm:justify-between">
|
||||
{prevLesson ? (
|
||||
<Link
|
||||
href={`${courseBase}/course/${prevLesson.moduleIndex}/${prevLesson.lessonIndex}`}
|
||||
className="flex items-center gap-2 rounded-xl border border-[var(--border)] bg-[var(--card)] px-4 py-3 text-sm font-medium transition hover:border-[var(--accent)]/50"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
上一节
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
{nextLesson ? (
|
||||
<Link
|
||||
href={`${courseBase}/course/${nextLesson.moduleIndex}/${nextLesson.lessonIndex}`}
|
||||
className="flex items-center gap-2 rounded-xl border border-[var(--border)] bg-[var(--card)] px-4 py-3 text-sm font-medium transition hover:border-[var(--accent)]/50 ml-auto"
|
||||
>
|
||||
下一节
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href={courseHomeHref}
|
||||
className="flex items-center gap-2 rounded-xl bg-[var(--accent)] px-4 py-3 text-sm font-medium text-[var(--accent-foreground)] transition hover:opacity-90 ml-auto"
|
||||
>
|
||||
返回课程首页
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user