This commit is contained in:
eric
2026-03-12 00:34:50 -05:00
parent 0c2ccadb6b
commit 5caf970c7e
26 changed files with 1528 additions and 288 deletions

31
lib/content/config.ts Normal file
View File

@@ -0,0 +1,31 @@
/**
* 共享内容模块配置 - 可标准应用到 digital / nomadvip / nomadlms
*/
export type EbookConfig = {
title: string;
backHref: string;
/** 是否需权限,免费则 false */
requireAuth?: boolean;
/** 预计阅读分钟数,用于倒计时/展示 */
estimatedMinutes?: number;
/** 存储 key 前缀,区分项目 */
storagePrefix?: string;
};
export type CourseConfig = {
courseId: string;
courseHomeHref: string;
payHref: string;
/** 试看:[[moduleIndex, lessonIndex], ...] 或 (m,l)=>boolean */
freeTrial?: [number, number][] | ((m: number, l: number) => boolean);
/** 视频进度存储前缀 */
videoStoragePrefix?: string;
/** 学习进度存储前缀 */
learningStoragePrefix?: string;
};
export type ContentConfig = {
ebook: EbookConfig;
course: CourseConfig;
};

View File

@@ -0,0 +1,26 @@
/**
* 课程工具 - 获取上一节/下一节
*/
export type LessonRef = { moduleIndex: number; lessonIndex: number };
export function getPrevNextLesson(
modules: readonly { lessons: readonly unknown[] }[],
moduleIndex: number,
lessonIndex: number
): { prev: LessonRef | null; next: LessonRef | null } {
const allLessons: LessonRef[] = [];
modules.forEach((m, mi) => {
m.lessons.forEach((_, li) => {
allLessons.push({ moduleIndex: mi, lessonIndex: li });
});
});
const idx = allLessons.findIndex((l) => l.moduleIndex === moduleIndex && l.lessonIndex === lessonIndex);
if (idx < 0) return { prev: null, next: null };
return {
prev: idx > 0 ? allLessons[idx - 1] : null,
next: idx < allLessons.length - 1 ? allLessons[idx + 1] : null,
};
}

51
lib/content/ebook.ts Normal file
View File

@@ -0,0 +1,51 @@
/**
* 电子书 Markdown 解析 - 共享模块
*/
import { marked } from "marked";
export interface EbookChunk {
html: string;
estimatedHeight: number;
}
export interface EbookData {
chunks: EbookChunk[];
headers: string[];
}
export function parseEbookMarkdown(source: string): EbookData {
const html = marked.parse(source, { breaks: true, gfm: true }) as string;
const lazyHtml = html.replace(/<img /g, '<img loading="lazy" decoding="async" ');
let rawChunks = lazyHtml.split(/(?=<h1[\s>])/i).filter((c) => c.trim());
if (rawChunks.length <= 1) {
rawChunks = lazyHtml.split(/(?=<h2[\s>])/i).filter((c) => c.trim());
}
const headers: string[] = [];
const headerRegex = /<h1[^>]*>(.*?)<\/h1>/gi;
let m;
while ((m = headerRegex.exec(lazyHtml)) !== null) {
headers.push(m[1].replace(/<[^>]+>/g, ""));
}
if (headers.length === 0) {
const h2Regex = /<h2[^>]*>(.*?)<\/h2>/gi;
while ((m = h2Regex.exec(lazyHtml)) !== null) {
headers.push(m[1].replace(/<[^>]+>/g, ""));
}
}
const chunks: EbookChunk[] = rawChunks.map((chunkHtml) => {
const imgCount = (chunkHtml.match(/<img\b/g) || []).length;
const textLen = chunkHtml.replace(/<[^>]+>/g, "").length;
const estimatedHeight = Math.max(300, Math.round(textLen * 0.6 + imgCount * 350));
return { html: chunkHtml, estimatedHeight };
});
return { chunks, headers };
}
/** 估算阅读时间(字/分钟,中文约 300-500 */
export function estimateReadingMinutes(textLength: number, wpm = 400): number {
return Math.max(1, Math.ceil(textLength / wpm));
}