167 lines
7.2 KiB
TypeScript
167 lines
7.2 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { useParams, useRouter } from "next/navigation";
|
||
import { Link, useTranslation } from "@/i18n/navigation";
|
||
import { VideoPlayer } from "@/app/components/course/VideoPlayer";
|
||
import { LessonMarkdown } from "./LessonMarkdown";
|
||
import { getLesson } from "@/config/lessons";
|
||
import { canWatchLesson } from "@/app/lib/course-payment";
|
||
import { useAuthAndVip } from "@/app/lib/useAuthAndVip";
|
||
import { setLastWatched, markCompleted } from "@/app/lib/learning";
|
||
import { DOWNLOAD_CONFIG } from "@/config/course";
|
||
import Header from "@/app/components/Header";
|
||
|
||
export default function LessonPage() {
|
||
const params = useParams();
|
||
const router = useRouter();
|
||
const { t } = useTranslation("community");
|
||
const { vip } = useAuthAndVip();
|
||
const [toast, setToast] = useState(false);
|
||
const moduleIndex = Number(params.moduleIndex);
|
||
const lessonIndex = Number(params.lessonIndex);
|
||
|
||
const lesson = getLesson(moduleIndex, lessonIndex);
|
||
const unlocked = canWatchLesson(moduleIndex, lessonIndex, vip);
|
||
const isFreeTrial = moduleIndex === 0 && lessonIndex < 2;
|
||
|
||
useEffect(() => {
|
||
if (lesson && unlocked) setLastWatched(moduleIndex, lessonIndex, lesson.name);
|
||
}, [moduleIndex, lessonIndex, lesson?.name, unlocked]);
|
||
|
||
if (!lesson || isNaN(moduleIndex) || isNaN(lessonIndex)) {
|
||
return (
|
||
<div className="min-h-screen bg-[var(--background)]">
|
||
<Header />
|
||
<main className="pt-20 pb-16">
|
||
<div className="mx-auto max-w-4xl px-4 py-16 text-center">
|
||
<h1 className="mb-4 text-2xl font-bold">章节不存在</h1>
|
||
<p className="mb-6 text-[var(--muted-foreground)]">
|
||
请检查链接或返回课程首页
|
||
</p>
|
||
<Link
|
||
href="/course"
|
||
className="inline-flex items-center gap-2 rounded-full bg-[var(--accent)] px-6 py-3 font-medium text-[var(--accent-foreground)] transition hover:opacity-90"
|
||
>
|
||
← 返回首页
|
||
</Link>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-[var(--background)] text-[var(--foreground)]">
|
||
<Header />
|
||
<main className="pt-14 pb-16 md:pt-16">
|
||
<div className="mx-auto max-w-4xl px-4 sm:px-6">
|
||
{/* 返回导航 */}
|
||
<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="/course" 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">
|
||
{lesson.name}
|
||
{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)]">
|
||
时长 {lesson.duration}
|
||
</p>
|
||
|
||
{/* 视频播放器 / 锁定 overlay */}
|
||
<div className="relative mb-12">
|
||
{unlocked ? (
|
||
<VideoPlayer
|
||
src={lesson.videoUrl}
|
||
title={lesson.name}
|
||
videoId={`course-${moduleIndex}-${lessonIndex}`}
|
||
className="shadow-xl"
|
||
onComplete={() => markCompleted(moduleIndex, lessonIndex)}
|
||
/>
|
||
) : (
|
||
<div className="relative overflow-hidden rounded-xl bg-black md:rounded-2xl">
|
||
<div className="aspect-video flex flex-col items-center justify-center gap-4 bg-zinc-900">
|
||
<span className="text-6xl">🔒</span>
|
||
<p className="text-lg text-white/90">报名解锁本章节</p>
|
||
<Link
|
||
href="/join"
|
||
className="rounded-full bg-[var(--accent)] px-6 py-3 font-medium text-[var(--accent-foreground)] transition hover:opacity-90"
|
||
>
|
||
立即报名
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 配套文档 - 仅解锁后显示 */}
|
||
{unlocked && (
|
||
<div className="relative rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm md:rounded-2xl md:p-8">
|
||
<h2 className="mb-6 flex items-center gap-2 text-lg font-semibold">
|
||
<svg className="h-5 w-5 text-[var(--accent)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||
</svg>
|
||
配套文档
|
||
</h2>
|
||
<LessonMarkdown content={lesson.markdown} />
|
||
</div>
|
||
)}
|
||
|
||
{/* 网盘下载 */}
|
||
{DOWNLOAD_CONFIG.enabled && unlocked && (
|
||
<div className="mt-8 rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm md:rounded-2xl md:p-8">
|
||
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold">
|
||
<svg className="h-5 w-5 text-[var(--accent)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||
</svg>
|
||
{DOWNLOAD_CONFIG.title}
|
||
</h2>
|
||
<p className="mb-4 text-sm text-[var(--muted-foreground)]">
|
||
课程配套资料已上传至网盘,报名后可前往下载
|
||
</p>
|
||
<a
|
||
href={DOWNLOAD_CONFIG.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="inline-flex items-center gap-2 rounded-full bg-[var(--accent)] px-6 py-3 font-medium text-[var(--accent-foreground)] transition hover:opacity-90"
|
||
>
|
||
{DOWNLOAD_CONFIG.buttonText}
|
||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||
</svg>
|
||
</a>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</main>
|
||
{toast && (
|
||
<div
|
||
className="fixed bottom-8 left-1/2 z-50 -translate-x-1/2 rounded-lg bg-slate-800 px-6 py-3 text-sm font-medium text-white shadow-lg dark:bg-slate-700"
|
||
role="alert"
|
||
>
|
||
{t("updating")}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|