152 lines
5.7 KiB
TypeScript
152 lines
5.7 KiB
TypeScript
"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>
|
|
);
|
|
}
|