diff --git a/app/cloudphone/config/course.ts b/app/cloudphone/config/course.ts index bbf4756..f0409ee 100644 --- a/app/cloudphone/config/course.ts +++ b/app/cloudphone/config/course.ts @@ -49,27 +49,27 @@ export const CURRICULUM_CONFIG = { emoji: "🚀", title: "基础篇 · 云手机入门", lessons: [ - { name: "01 什么是云手机?自建 vs 商业平台", duration: "08:32" }, - { name: "02 低成本工作室硬件选型", duration: "12:15" }, + { name: "01 从0到1:安卓镜像安装与首次开机", duration: "08:32" }, + { name: "02 性能翻倍:安卓存储扩容实操", duration: "12:15" }, ], }, { emoji: "⚙️", title: "技术篇 · Redroid 与 AOSP", lessons: [ - { name: "03 Docker 虚拟化安卓 - Redroid", duration: "15:20" }, - { name: "04 ARM 开发板 AOSP 系统", duration: "18:00" }, - { name: "05 相机与推流配置", duration: "14:30" }, + { name: "03 竖屏直播必备:系统显示方向设置", duration: "15:20" }, + { name: "04 一次搞定 Root:Magisk + 模块安装", duration: "18:00" }, + { name: "05 防风控第一步:机型参数改造", duration: "14:30" }, ], }, { emoji: "📺", title: "直播篇 · 无人推流", lessons: [ - { name: "06 ffmpeg 拉流转推", duration: "16:00" }, - { name: "07 SRS 直播服务器", duration: "12:45" }, - { name: "08 油猴脚本自动化", duration: "11:20" }, - { name: "09 TikTok/Youtube 实战", duration: "20:00" }, + { name: "06 降低风控命中:GPS 精准定位", duration: "16:00" }, + { name: "07 无人直播入门:录播素材循环推流", duration: "12:45" }, + { name: "08 TikTok 实战开播:完整流程演示", duration: "11:20" }, + { name: "09 TikTok 进阶优化:稳定推流与细节调优", duration: "20:00" }, ], }, { diff --git a/app/cloudphone/config/lessons.ts b/app/cloudphone/config/lessons.ts index f6e308a..0a7beb8 100644 --- a/app/cloudphone/config/lessons.ts +++ b/app/cloudphone/config/lessons.ts @@ -4,49 +4,286 @@ const SAMPLE_VIDEO = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; -const SAMPLE_MARKDOWN = `# 课程概述 +const LESSON_MARKDOWNS: Record = { + "0-0": `# 01 从0到1:安卓镜像安装与首次开机 -本小节将带你理解云手机的核心概念,掌握低成本工作室搭建技能。 +对应电子书章节:\`硬件选择\` -> \`刷入安卓系统\`。 -## 学习目标 +## 你会完成什么 -- 理解云手机的本质与自建方案 -- 掌握 Redroid、AOSP 等虚拟化技术 -- 完成无人直播推流配置 +- 准备 TF 卡、读卡器、开发板电源与网线 +- 下载开发板安卓镜像(AOSP) +- 用 Balena Etcher 刷写 img 并首次开机 -## 核心要点 +## 实操步骤 -1. **定义**:云手机 = 虚拟化安卓 + 远程控制 + 自动化推流 -2. **技术**:Docker Redroid、ARM 开发板、ffmpeg、SRS -3. **成本**:自建单台成本可控制在千元以内 +1. 将 img 镜像写入 TF 卡 +2. TF 卡插回开发板,接通电源和网线 +3. 首次启动后确认系统已进入桌面并可调试 -## 行动清单 +## 避坑提醒 -- [ ] 评估自己的硬件与网络环境 -- [ ] 列出想实现的直播平台(TikTok/Youtube 等) -- [ ] 设定 1 个月内的搭建目标 +- 刷机前优先确认镜像版本与板型匹配 +- 首次开机时间可能较长,不要中途断电 +`, + "0-1": `# 02 性能翻倍:安卓存储扩容实操 -### 代码示例 +对应电子书章节:\`硬件选择\` 与存储相关说明(TF/SSD 组合)。 + +## 核心目标 + +- 解决默认系统空间不足导致的安装失败 +- 保证后续安装模块、素材、脚本有足够空间 + +## 推荐方案 + +- TF 卡至少 64GB +- 优先使用 USB3.0 读卡器提升刷写效率 +- 有条件时搭配 SSD 作为素材盘 + +## 执行清单 + +- [ ] 检查系统可用容量 +- [ ] 清理无用预装与缓存 +- [ ] 预留直播素材和日志空间 +`, + "1-0": `# 03 竖屏直播必备:系统显示方向设置 + +对应电子书章节:\`业务场景:无人直播\` 中摄像头与画面链路实践。 + +## 为什么先做竖屏 + +- TikTok 直播以竖屏为主 +- 先固定方向,后续采集与推流才稳定 + +## 操作要点 + +1. 在系统中关闭自动旋转 +2. 固定竖屏分辨率 +3. 使用测试素材验证采集画面方向 + +## 验证标准 + +- 开播预览无横竖屏抖动 +- 推流端与手机端画面方向一致 +`, + "1-1": `# 04 一次搞定 Root:Magisk + 模块安装 + +对应电子书章节:\`获取root权限\` 与 \`安装lsposed环境\`。 + +## 实操流程 + +1. 从镜像提取 \`boot.img\` +2. 在安卓端用 Magisk 修补镜像 +3. 通过刷写工具或 adb dd 回刷补丁镜像 +4. 重启验证 Root 生效 + +## 关键命令(电子书同源) \`\`\`bash -# 启动 Redroid 容器 -docker run -itd --privileged \\ - -v ~/data:/data \\ - redroid/redroid:13.0.0-latest +adb shell +su +dd if=/sdcard/magisk_patched.img of=/dev/block/mmcblk0p9 bs=4M +sync +reboot \`\`\` -行内代码示例:\`ffmpeg -re -i input.mp4 -c copy -f flv rtmp://server/live\` 可推流到 RTMP 服务器。 +## 后续动作 -## 注意事项 +- 安装 LSPosed,准备模块化能力 +- 记录可回滚镜像,避免系统损坏 +`, + "1-2": `# 05 防风控第一步:机型参数改造 -> 云手机自建需要一定技术基础,建议先完成前 2 节试看再决定是否购买。 +对应电子书章节:\`修改机型\`。 -| 项目 | 说明 | -|------|------| -| 硬件 | 建议 ARM 开发板或 x86 主机 | -| 网络 | 需科学上网与静态 IP | -| 时间 | 完整搭建约 1-2 天 | -`; +## 目标 + +- 降低平台对异常设备指纹的识别概率 +- 让账号环境更贴近真实移动设备 + +## 建议流程 + +1. 安装改机工具并授予 Root 权限 +2. 进入设备信息伪装,选择目标机型 +3. 重启系统并重新检查参数是否生效 + +## 风险提示 + +- 机型参数、IP、GPS 需要协同一致 +- 不建议频繁切换机型,避免触发异常行为 +`, + "2-0": `# 06 降低风控命中:GPS 精准定位 + +对应电子书章节:\`GPS定位\`。 + +## 为什么必须做 + +- GPS 与 IP 国家/城市不一致,容易触发风控 +- 直播账号稳定性与定位一致性强相关 + +## 操作步骤 + +1. 安装定位工具并配置 Mock Location +2. 通过地图搜索目标区域并手动校准 +3. 启动模拟定位后再打开直播 App + +## 检查清单 + +- [ ] GPS 城市与代理 IP 所在地区一致 +- [ ] 时间时区与账号地区一致 +- [ ] 开播前再次确认定位未漂移 +`, + "2-1": `# 07 无人直播入门:录播素材循环推流 + +对应电子书章节:\`业务场景:无人直播\` 与 \`录播mp4素材\`。 + +## 核心链路 + +1. Linux 辅助板播放素材 +2. HDMI 输出到采集卡 +3. 采集卡通过 USB 输入安卓板,识别为摄像头 + +## 示例命令 + +\`\`\`bash +ffplay -loop 0 -vf "transpose=2,scale=640:-1,format=yuv420p" -fs -threads 6 xxx.mp4 +\`\`\` + +## 实战建议 + +- 先用短素材跑通链路,再换长素材 +- 分辨率和码率不要一步拉满,先求稳定 +`, + "2-2": `# 08 TikTok 实战开播:完整流程演示 + +对应电子书章节:\`业务场景:无人直播\` 与 \`辅助板linux配置\`。 + +## 开播前准备 + +- 辅助板已安装视频/音频驱动与 ffmpeg +- 安卓端已完成 Root、改机、定位 +- 直播账号完成基础养号与资料设置 + +## 标准流程 + +1. 启动素材源或直播流 +2. 在 TikTok 端预览并确认摄像头输入 +3. 开播后观察前 10 分钟数据与风控提示 + +## 观察重点 + +- 是否弹验证窗口 +- 是否出现明显限流 +- 进房、停留、互动是否正常 +`, + "2-3": `# 09 TikTok 进阶优化:稳定推流与细节调优 + +对应电子书章节:\`obs推流/srs拉流\`。 + +## 进阶目标 + +- 降低延迟与卡顿 +- 提升长时段推流稳定性 + +## SRS 部署思路 + +- 在辅助板部署 SRS +- OBS 走 SRT 推流,辅助板本地拉流喂给安卓摄像头 + +## 拉流脚本示例 + +\`\`\`bash +ffplay -fflags nobuffer -flags low_delay -framedrop \ +-probesize 32 -analyzeduration 0 -sync ext -infbuf \ +-vf "setpts=PTS-STARTPTS" \ +'srt://127.0.0.1:10080?streamid=#!::r=live/livestream,m=request&latency=10' +\`\`\` +`, + "3-0": `# 10 软路由与科学上网 + +对应电子书章节:\`魔法上网\`。 + +## 为什么是软路由优先 + +- 电子书实测:代理 APK 在部分场景难过风控 +- 软路由链路更稳定,适合长期业务运行 + +## 推荐组合 + +- 海外 VPS(中转) +- 静态住宅 IP(落地) +- Clash Meta(链式代理) + +## 操作顺序 + +1. 购买 VPS 与域名 +2. 购买静态住宅 IP +3. 配置链式代理与策略路由 +`, + "3-1": `# 11 静态住宅 IP 配置 + +对应电子书章节:\`静态住宅ip\` 与 \`链式代理\`。 + +## 核心原则 + +- 便宜节点通常不能国内直连,必须走中转 +- IP、GPS、机型要形成一致的“环境画像” + +## 配置要点 + +- 落地节点使用静态住宅 IP +- 中转与落地分层,避免单点故障 +- 业务账号固定绑定线路,不频繁切换 + +## 验证指标 + +- DNS、出口国家、时区一致 +- 连续开播无大规模验证弹窗 +`, + "4-0": `# 12 HDMI 辅助板方案 + +对应电子书章节:\`如果需要直播\` 与 \`业务场景:无人直播\`。 + +## 方案价值 + +- 把外部视频信号稳定送进安卓端“摄像头” +- 满足 TikTok 对移动端直播形态的偏好 + +## 硬件清单 + +- Linux 辅助板(如 R3S) +- 视频采集卡 +- HDMI 线 +- 安卓开发板(主控) + +## 连接示意 + +\`素材源 -> HDMI -> 采集卡 -> 安卓板 USB\` + +确认安卓相机可预览即代表链路打通。 +`, + "4-1": `# 13 多设备群控与远控 + +对应电子书章节:\`远程控制\`、\`内网穿透 frp\`、\`应用多开/系统多开\`。 + +## 目标 + +- 一台电脑管理多台安卓设备 +- 降低人工重复操作成本 + +## 常见工具链 + +- qtscrcpy / escrcpy:图形远控与群控 +- frp-panel:外网访问与设备穿透 +- 安卓虚拟机:低成本扩展多开能力 + +## 交付标准 + +- 可以批量连接设备并执行统一操作 +- 关键节点支持日志与异常回溯 +`, +}; export type LessonDetail = { name: string; @@ -57,82 +294,82 @@ export type LessonDetail = { export const LESSONS_MAP: Record = { "0-0": { - name: "01 什么是云手机?自建 vs 商业平台", + name: "01 从0到1:安卓镜像安装与首次开机", duration: "08:32", - videoUrl: SAMPLE_VIDEO, - markdown: SAMPLE_MARKDOWN.replace("本小节", "本节").replace("核心概念", "云手机定义与自建 vs 商业平台对比"), + videoUrl: "https://minioweb.hackrobot.cn/hackrobot/1. 安装安卓img_20260326_095335_848.mp4", + markdown: LESSON_MARKDOWNS["0-0"], }, "0-1": { - name: "02 低成本工作室硬件选型", + name: "02 性能翻倍:安卓存储扩容实操", duration: "12:15", - videoUrl: SAMPLE_VIDEO, - markdown: SAMPLE_MARKDOWN.replace("本小节", "本节").replace("核心概念", "开发板、采集卡、配件选型"), + videoUrl: "https://minioweb.hackrobot.cn/hackrobot/2. 安卓扩容_20260326_095426_1002.mp4", + markdown: LESSON_MARKDOWNS["0-1"], }, "1-0": { - name: "03 Docker 虚拟化安卓 - Redroid", + name: "03 竖屏直播必备:系统显示方向设置", duration: "15:20", - videoUrl: SAMPLE_VIDEO, - markdown: SAMPLE_MARKDOWN.replace("本小节", "本节").replace("核心概念", "Redroid 安装与配置"), + videoUrl: "https://minioweb.hackrobot.cn/hackrobot/3. 设置竖屏_20260326_095414_979.mp4", + markdown: LESSON_MARKDOWNS["1-0"], }, "1-1": { - name: "04 ARM 开发板 AOSP 系统", + name: "04 一次搞定 Root:Magisk + 模块安装", duration: "18:00", - videoUrl: SAMPLE_VIDEO, - markdown: SAMPLE_MARKDOWN.replace("本小节", "本节").replace("核心概念", "AOSP 刷机与系统定制"), + videoUrl: "https://minioweb.hackrobot.cn/hackrobot/4. 安装magisk以及模块_20260326_095427_1018.mp4", + markdown: LESSON_MARKDOWNS["1-1"], }, "1-2": { - name: "05 相机与推流配置", + name: "05 防风控第一步:机型参数改造", duration: "14:30", - videoUrl: SAMPLE_VIDEO, - markdown: SAMPLE_MARKDOWN.replace("本小节", "本节").replace("核心概念", "虚拟相机与 RTMP 推流"), + videoUrl: "https://minioweb.hackrobot.cn/hackrobot/5. 修改机型参数_20260326_095438_1051.mp4", + markdown: LESSON_MARKDOWNS["1-2"], }, "2-0": { - name: "06 ffmpeg 拉流转推", + name: "06 降低风控命中:GPS 精准定位", duration: "16:00", - videoUrl: SAMPLE_VIDEO, - markdown: SAMPLE_MARKDOWN.replace("本小节", "本节").replace("核心概念", "ffmpeg 拉流与转推"), + videoUrl: "https://minioweb.hackrobot.cn/hackrobot/6. gps定位_20260326_095430_1031.mp4", + markdown: LESSON_MARKDOWNS["2-0"], }, "2-1": { - name: "07 SRS 直播服务器", + name: "07 无人直播入门:录播素材循环推流", duration: "12:45", - videoUrl: SAMPLE_VIDEO, - markdown: SAMPLE_MARKDOWN.replace("本小节", "本节").replace("核心概念", "SRS 部署与推拉流"), + videoUrl: "https://minioweb.hackrobot.cn/hackrobot/7无人直播-录播_20260326_100335_2814.mp4", + markdown: LESSON_MARKDOWNS["2-1"], }, "2-2": { - name: "08 油猴脚本自动化", + name: "08 TikTok 实战开播:完整流程演示", duration: "11:20", - videoUrl: SAMPLE_VIDEO, - markdown: SAMPLE_MARKDOWN.replace("本小节", "本节").replace("核心概念", "油猴脚本实现自动化操作"), + videoUrl: "https://minioweb.hackrobot.cn/hackrobot/8. tiktok直播_20260326_095441_1064.mp4", + markdown: LESSON_MARKDOWNS["2-2"], }, "2-3": { - name: "09 TikTok/Youtube 实战", + name: "09 TikTok 进阶优化:稳定推流与细节调优", duration: "20:00", - videoUrl: SAMPLE_VIDEO, - markdown: SAMPLE_MARKDOWN.replace("本小节", "本节").replace("核心概念", "TikTok/Youtube 无人直播实战"), + videoUrl: "https://minioweb.hackrobot.cn/hackrobot/9. tiktok直播-2_20260326_095403_943.mp4", + markdown: LESSON_MARKDOWNS["2-3"], }, "3-0": { name: "10 软路由与科学上网", duration: "13:00", videoUrl: SAMPLE_VIDEO, - markdown: SAMPLE_MARKDOWN.replace("本小节", "本节").replace("核心概念", "软路由配置与科学上网"), + markdown: LESSON_MARKDOWNS["3-0"], }, "3-1": { name: "11 静态住宅 IP 配置", duration: "10:30", videoUrl: SAMPLE_VIDEO, - markdown: SAMPLE_MARKDOWN.replace("本小节", "本节").replace("核心概念", "静态住宅 IP 与代理配置"), + markdown: LESSON_MARKDOWNS["3-1"], }, "4-0": { name: "12 HDMI 辅助板方案", duration: "15:00", videoUrl: SAMPLE_VIDEO, - markdown: SAMPLE_MARKDOWN.replace("本小节", "本节").replace("核心概念", "HDMI 采集与辅助板"), + markdown: LESSON_MARKDOWNS["4-0"], }, "4-1": { name: "13 多设备群控与远控", duration: "12:00", videoUrl: SAMPLE_VIDEO, - markdown: SAMPLE_MARKDOWN.replace("本小节", "本节").replace("核心概念", "群控与远程控制方案"), + markdown: LESSON_MARKDOWNS["4-1"], }, }; diff --git a/app/data/vip.mock.ts b/app/data/vip.mock.ts index 14c2ed6..d087cfa 100644 --- a/app/data/vip.mock.ts +++ b/app/data/vip.mock.ts @@ -63,7 +63,7 @@ export const EBOOKS_DISPLAY = [ /** 视频教程展示 - 参考 digital.hackrobot.cn 资源导航专题,至少 4 个 */ export const VIDEO_COURSES_DISPLAY = [ - { id: "cloudphone", icon: "☁️", title: "云手机", desc: "低成本自建云手机,TikTok/YouTube 运营搭建指南", href: "/cloudphone" }, + { id: "cloudphone", icon: "☁️", title: "云手机", desc: "低成本自建云手机,TikTok/YouTube 无人直播实战指南", href: "/cloudphone" }, { id: "livestream", icon: "📺", title: "无人直播", desc: "TikTok/YouTube 无人直播,24 小时自动化", href: "/topic/livestream" }, { id: "indie", icon: "🛠️", title: "独立开发", desc: "从想法到产品,独立开发者全流程指南", href: "/topic/indie" }, { id: "aiagent", icon: "🤖", title: "AI Agent", desc: "AIGC 与智能体,Vibe Coding 跨界创造", href: "/topic/aiagent" }, diff --git a/app/topic/components/TopicCurriculum.tsx b/app/topic/components/TopicCurriculum.tsx index f280e6d..fe6148a 100644 --- a/app/topic/components/TopicCurriculum.tsx +++ b/app/topic/components/TopicCurriculum.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import Link from "next/link"; import { getTopicCurriculum } from "../config"; -import { canWatchLesson } from "@/app/cloudphone/lib/payment"; +import { canWatchLesson, isPaid } from "@/app/cloudphone/lib/payment"; import { isBookmarked, toggleBookmark, getBookmarks, getCompletedLessons } from "@/app/cloudphone/lib/learning"; type TopicCurriculumProps = { @@ -14,19 +14,26 @@ export function TopicCurriculum({ topicId }: TopicCurriculumProps) { const [expanded, setExpanded] = useState>({}); const [bookmarks, setBookmarks] = useState>(new Set()); const [completed, setCompleted] = useState>(new Set()); + const [paid, setPaid] = useState(false); + const [showUpdatingModal, setShowUpdatingModal] = useState(false); + const [updatingLessonName, setUpdatingLessonName] = useState(""); const curriculum = getTopicCurriculum(topicId); useEffect(() => { setBookmarks(getBookmarks()); setCompleted(getCompletedLessons()); + setPaid(isPaid()); const onB = () => setBookmarks(getBookmarks()); const onP = () => setCompleted(getCompletedLessons()); + const onPayUpdate = () => setPaid(isPaid()); window.addEventListener("learning:bookmarks", onB); window.addEventListener("learning:progress", onP); + window.addEventListener("pay:updated", onPayUpdate); return () => { window.removeEventListener("learning:bookmarks", onB); window.removeEventListener("learning:progress", onP); + window.removeEventListener("pay:updated", onPayUpdate); }; }, []); @@ -43,6 +50,20 @@ export function TopicCurriculum({ topicId }: TopicCurriculumProps) { } }, [topicId]); + useEffect(() => { + if (!showUpdatingModal) return; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") setShowUpdatingModal(false); + }; + const prevOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + window.addEventListener("keydown", onKeyDown); + return () => { + document.body.style.overflow = prevOverflow; + window.removeEventListener("keydown", onKeyDown); + }; + }, [showUpdatingModal]); + const toggle = (i: number) => { setExpanded((prev) => ({ ...prev, [i]: !prev[i] })); }; @@ -50,6 +71,17 @@ export function TopicCurriculum({ topicId }: TopicCurriculumProps) { if (!curriculum?.hasContent || !curriculum.modules.length) return null; const modules = curriculum.modules; + const isLivestreamTopic = topicId === "livestream"; + const isLivestreamUpdating = isLivestreamTopic && paid; + + const openUpdatingModal = (lessonName: string) => { + setUpdatingLessonName(lessonName); + setShowUpdatingModal(true); + }; + + const closeUpdatingModal = () => { + setShowUpdatingModal(false); + }; return (
@@ -92,12 +124,50 @@ export function TopicCurriculum({ topicId }: TopicCurriculumProps) { const key = `${origMod}-${li}`; const bookmarked = isBookmarked(origMod, li); const isCompleted = completed.has(key); + const textClass = unlocked ? "text-[var(--foreground)]" : "text-[var(--muted-foreground)]"; return (
  • - + {isLivestreamUpdating ? ( +
    { + if (paid) openUpdatingModal(lesson.name); + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + if (paid) openUpdatingModal(lesson.name); + } + }} + className="flex w-full flex-col gap-1 px-4 py-3 text-left transition hover:bg-[var(--muted)]/50 sm:flex-row sm:items-center sm:justify-between sm:px-5 sm:py-3" + > + + + {lesson.name} + 🛠️ + + + {lesson.duration} + ··· + +
    + ) : ( + - - {lesson.name} - + {lesson.name} {free && ( 试看 @@ -136,7 +200,8 @@ export function TopicCurriculum({ topicId }: TopicCurriculumProps) { {lesson.duration} - + + )}
  • ); })} @@ -147,6 +212,49 @@ export function TopicCurriculum({ topicId }: TopicCurriculumProps) { })} + {showUpdatingModal && ( +
    +
    +
    e.stopPropagation()} + > +
    + 🚧 +
    +

    正在更新

    +

    + {updatingLessonName} 正在升级内容, + 将在完成后第一时间开放。 +

    +

    + 现在可以先学习「云手机」课程,后续会自动同步最新章节。 +

    +
    + + +
    +
    +
    + )}
    ); } diff --git a/app/topic/config.ts b/app/topic/config.ts index 79ff15f..584e37d 100644 --- a/app/topic/config.ts +++ b/app/topic/config.ts @@ -44,7 +44,6 @@ export const TOPIC_CONFIGS: Record = { { value: "100+", label: "分钟视频" }, { value: "2", label: "大模块" }, ], - moduleIndices: [1, 2], payUrl: "/", }, indie: { @@ -75,6 +74,36 @@ export const TOPIC_CONFIGS: Record = { }, }; +const LIVESTREAM_CURRICULUM = [ + { + emoji: "🎯", + title: "策略篇 · 账号定位", + lessons: [ + { name: "01 赛道选择与账号人设设计", duration: "09:30" }, + { name: "02 起号期内容与直播节奏", duration: "11:20" }, + ], + originalModuleIndex: 1, + }, + { + emoji: "🧩", + title: "执行篇 · 素材与脚本", + lessons: [ + { name: "03 直播素材筛选与版权避坑", duration: "12:10" }, + { name: "04 开播话术与互动脚本模板", duration: "10:45" }, + ], + originalModuleIndex: 2, + }, + { + emoji: "📈", + title: "增长篇 · 复盘优化", + lessons: [ + { name: "05 流量数据复盘与调优闭环", duration: "13:15" }, + { name: "06 多账号矩阵扩展方法", duration: "14:00" }, + ], + originalModuleIndex: 3, + }, +] as const; + export function getTopicConfig(topicId: string): TopicConfig | null { return TOPIC_CONFIGS[topicId] ?? null; } @@ -83,6 +112,9 @@ export function getTopicCurriculum(topicId: string) { const config = getTopicConfig(topicId); if (!config) return null; if (config.externalUrl) return { modules: [], hasContent: false }; + if (topicId === "livestream") { + return { modules: LIVESTREAM_CURRICULUM, hasContent: LIVESTREAM_CURRICULUM.length > 0 }; + } const indices = config.moduleIndices ?? CURRICULUM_CONFIG.modules.map((_, i) => i); const modules = indices .map((i) => ({ module: CURRICULUM_CONFIG.modules[i], originalIndex: i }))