's'
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -14,8 +14,8 @@
|
|||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
.next/
|
||||||
/out/
|
out/
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Link, useTranslation } from "@/i18n/navigation";
|
|||||||
import { VideoPlayer } from "@/app/components/course/VideoPlayer";
|
import { VideoPlayer } from "@/app/components/course/VideoPlayer";
|
||||||
import { LessonMarkdown } from "./LessonMarkdown";
|
import { LessonMarkdown } from "./LessonMarkdown";
|
||||||
import { getLesson } from "@/config/lessons";
|
import { getLesson } from "@/config/lessons";
|
||||||
import { canWatchLesson } from "@/app/lib/course-payment";
|
import { canWatchLesson, isFreeTrial } from "@/app/lib/course-payment";
|
||||||
import { useAuthAndVip } from "@/app/lib/useAuthAndVip";
|
import { useAuthAndVip } from "@/app/lib/useAuthAndVip";
|
||||||
import { getStoredToken, getStoredUser } from "@/app/lib/pocketbase";
|
import { getStoredToken, getStoredUser } from "@/app/lib/pocketbase";
|
||||||
import { setLastWatched, markCompleted } from "@/app/lib/learning";
|
import { setLastWatched, markCompleted } from "@/app/lib/learning";
|
||||||
@@ -31,7 +31,6 @@ export default function LessonPage() {
|
|||||||
|
|
||||||
const lesson = getLesson(moduleIndex, lessonIndex);
|
const lesson = getLesson(moduleIndex, lessonIndex);
|
||||||
const unlocked = canWatchLesson(moduleIndex, lessonIndex, vip);
|
const unlocked = canWatchLesson(moduleIndex, lessonIndex, vip);
|
||||||
const isFreeTrial = moduleIndex === 0 && lessonIndex < 2;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lesson && unlocked) setLastWatched(moduleIndex, lessonIndex, lesson.name);
|
if (lesson && unlocked) setLastWatched(moduleIndex, lessonIndex, lesson.name);
|
||||||
@@ -146,7 +145,7 @@ export default function LessonPage() {
|
|||||||
|
|
||||||
<h1 className="mb-2 flex items-center gap-2 text-xl font-bold sm:text-2xl md:text-3xl">
|
<h1 className="mb-2 flex items-center gap-2 text-xl font-bold sm:text-2xl md:text-3xl">
|
||||||
{lesson.name}
|
{lesson.name}
|
||||||
{isFreeTrial && (
|
{isFreeTrial(moduleIndex, lessonIndex) && (
|
||||||
<span className="rounded bg-[var(--accent)]/20 px-2 py-0.5 text-sm font-normal text-[var(--accent)]">
|
<span className="rounded bg-[var(--accent)]/20 px-2 py-0.5 text-sm font-normal text-[var(--accent)]">
|
||||||
试看
|
试看
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState, useEffect } from "react";
|
|||||||
import { Link } from "@/i18n/navigation";
|
import { Link } from "@/i18n/navigation";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
import { CURRICULUM_CONFIG } from "@/config/course";
|
import { CURRICULUM_CONFIG } from "@/config/course";
|
||||||
import { canWatchLesson } from "@/app/lib/course-payment";
|
import { canWatchLesson, isFreeTrial } from "@/app/lib/course-payment";
|
||||||
import { useAuthAndVip } from "@/app/lib/useAuthAndVip";
|
import { useAuthAndVip } from "@/app/lib/useAuthAndVip";
|
||||||
import { isBookmarked, toggleBookmark, getBookmarks, getCompletedLessons } from "@/app/lib/learning";
|
import { isBookmarked, toggleBookmark, getBookmarks, getCompletedLessons } from "@/app/lib/learning";
|
||||||
|
|
||||||
@@ -99,7 +99,6 @@ export function CurriculumSection() {
|
|||||||
{isOpen && (
|
{isOpen && (
|
||||||
<ul className="border-t border-[var(--border)]">
|
<ul className="border-t border-[var(--border)]">
|
||||||
{c.lessons.map((l) => {
|
{c.lessons.map((l) => {
|
||||||
const free = l.moduleIndex === 0 && l.lessonIndex < 2;
|
|
||||||
const unlocked = canWatchLesson(l.moduleIndex, l.lessonIndex, vip);
|
const unlocked = canWatchLesson(l.moduleIndex, l.lessonIndex, vip);
|
||||||
const key = `${l.moduleIndex}-${l.lessonIndex}`;
|
const key = `${l.moduleIndex}-${l.lessonIndex}`;
|
||||||
const bookmarked = isBookmarked(l.moduleIndex, l.lessonIndex);
|
const bookmarked = isBookmarked(l.moduleIndex, l.lessonIndex);
|
||||||
@@ -126,7 +125,7 @@ export function CurriculumSection() {
|
|||||||
<span className={unlocked ? "text-[var(--foreground)]" : "text-[var(--muted-foreground)]"}>
|
<span className={unlocked ? "text-[var(--foreground)]" : "text-[var(--muted-foreground)]"}>
|
||||||
{l.name}
|
{l.name}
|
||||||
</span>
|
</span>
|
||||||
{free && (
|
{isFreeTrial(l.moduleIndex, l.lessonIndex) && (
|
||||||
<span className="shrink-0 rounded bg-[var(--accent)]/20 px-1.5 py-0.5 text-xs text-[var(--accent)]">
|
<span className="shrink-0 rounded bg-[var(--accent)]/20 px-1.5 py-0.5 text-xs text-[var(--accent)]">
|
||||||
试看
|
试看
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
* 与 join 报名支付分离,课程解锁用 VIP 状态
|
* 与 join 报名支付分离,课程解锁用 VIP 状态
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** 前 2 章为试看,无需支付 */
|
/** 前 2 章试看(暂时关闭,全部需 VIP 解锁) */
|
||||||
export function isFreeTrial(moduleIndex: number, lessonIndex: number): boolean {
|
export function isFreeTrial(_moduleIndex: number, _lessonIndex: number): boolean {
|
||||||
if (moduleIndex === 0 && lessonIndex < 2) return true;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export const FAQ_CONFIG = {
|
|||||||
title: "常见问题",
|
title: "常见问题",
|
||||||
items: [
|
items: [
|
||||||
{ q: "课程可以永久回看吗?", a: "可以,报名后永久有效,随时回看。" },
|
{ q: "课程可以永久回看吗?", a: "可以,报名后永久有效,随时回看。" },
|
||||||
{ q: "适合零基础吗?", a: "适合。课程从入门到进阶,前 2 节可免费试看。" },
|
{ q: "适合零基础吗?", a: "适合。课程从入门到进阶;视频试看暂时关闭,报名后可解锁全部章节。" },
|
||||||
{ q: "有社群答疑吗?", a: "有,报名后加入专属社群,导师和同学一起交流。" },
|
{ q: "有社群答疑吗?", a: "有,报名后加入专属社群,导师和同学一起交流。" },
|
||||||
{ q: "支持退款吗?", a: "7 天内不满意可申请全额退款。" },
|
{ q: "支持退款吗?", a: "7 天内不满意可申请全额退款。" },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
/** 由 scripts/build-safe.cjs 设置;在 VPS 上也可手动 export 后执行 pnpm build */
|
||||||
|
const lowMemBuild = process.env.NEXT_BUILD_LOW_MEM === "1";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
experimental: {
|
experimental: {
|
||||||
serverActions: { bodySizeLimit: "25mb" },
|
serverActions: { bodySizeLimit: "25mb" },
|
||||||
|
...(lowMemBuild
|
||||||
|
? {
|
||||||
|
// 限制并行与预渲染并发,降低峰值内存(Next 16 默认 Turbopack 构建)
|
||||||
|
cpus: 1,
|
||||||
|
turbopackMemoryLimit: 1536 * 1024 * 1024,
|
||||||
|
staticGenerationMaxConcurrency: 1,
|
||||||
|
webpackMemoryOptimizations: true,
|
||||||
|
parallelServerCompiles: false,
|
||||||
|
parallelServerBuildTraces: false,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
async headers() {
|
async headers() {
|
||||||
const noCacheHeaders = [
|
const noCacheHeaders = [
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3000 -H 0.0.0.0",
|
"dev": "next dev -p 3000 -H 0.0.0.0",
|
||||||
"build": "next build",
|
"build": "node ./scripts/build-safe.cjs",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
|
|||||||
30
scripts/build-safe.cjs
Normal file
30
scripts/build-safe.cjs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 低端 VPS / 小内存环境:限制 Node 堆、开启 Next 低内存构建选项。
|
||||||
|
* 用法:pnpm build(默认经本脚本)
|
||||||
|
* 可选环境变量:
|
||||||
|
* NEXT_BUILD_HEAP_MB — Node --max-old-space-size(默认 1536)
|
||||||
|
*/
|
||||||
|
const { spawnSync } = require("child_process");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const heapMb = process.env.NEXT_BUILD_HEAP_MB || "1536";
|
||||||
|
const nextBin = path.join(
|
||||||
|
path.dirname(require.resolve("next/package.json")),
|
||||||
|
"dist",
|
||||||
|
"bin",
|
||||||
|
"next"
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
["--max-old-space-size=" + heapMb, nextBin, "build"],
|
||||||
|
{
|
||||||
|
stdio: "inherit",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
NEXT_BUILD_LOW_MEM: "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(result.status ?? 1);
|
||||||
Reference in New Issue
Block a user