AI 入门实战训练营 · 课程讲义

学生版用法:每一步只看「目标 → 界面操作 → 🤖 提示词 → ✅」。代码和命令已收进「给 AI 的参考资料」;点 复制提示词 会自动带上,直接粘贴给 AI 即可。

用 AI 工具做一个「敬拜团歌谱编辑系统」

课程目标:完成实战项目「敬拜团歌谱编辑系统」,并部署上线。 讲义只保留你动手实现这个目标所需的内容。

这份讲义是你的操作手册。按顺序完成每一步,即可将支持登录、编辑歌谱、打印和实时讨论的网站部署上线。


0. 课程速览

项目 内容
课程名称 AI 入门实战训练营:用 AI Agent 做出你的第一个网站和 App
本期项目 敬拜团歌谱编辑系统(新建歌曲、编辑歌词与和弦、上传歌谱图片/PDF、打印、多账号登录、实时评论)
上课时间 连续三个周六:11 月 7 日、11 月 14 日、11 月 21 日,下午 2:00 – 6:00
上课地点 教会 0316 室
课程形式 第 1 次:讲课日,老师讲解全部内容并演示完整流程;第 2、3 次:辅导日,你依据讲义动手实践,老师现场辅导(详见 0.3)
适合谁 想了解 AI 应用、希望开发实用工具的学员
自带 笔记本电脑(支持 Windows / Mac / Linux)与充电器
咨询 WhatsApp 马伦 81311318

⏱ 每次课共 4 小时;辅导日的里程碑时间仅供参考,可按自己的进度灵活安排。

0.2 学完你能做到

  1. 理解 AI Agent(AI 编程助手)概念,学会使用中文指令编写代码与排查问题。
  2. 学会使用 GitHub 保存代码,通过版本提交管理修改历史,支持随时回退。
  3. 学会把 Next.js 网站部署到 Cloudflare,获取公开访问网址。
  4. 学会使用 Cloudflare D1 存储歌曲信息,使用 R2 存储歌谱图片与 PDF 文件。
  5. 学会使用 Firebase 实现账号登录,并使用 Firebase Realtime Database 实现实时评论与在线状态展示。
  6. 学会排查程序报错,将报错信息提供给 AI 并按步骤排查解决。

0.3 三次课怎么安排(先看这一节)

日期 形式 谁做什么 结束时的目标
第一次课 11 月 7 日 讲课日:老师讲解全部内容并演示完整流程 老师讲解与演示;学员听讲与记录,不需要跟着敲命令 了解项目从创建到上线的全流程及各云服务分工;课后完成开发环境配置
第二次课 11 月 14 日 辅导日:无新讲授内容 你依据讲义第 8 章(阶段 1–6)动手实践;老师现场辅导 网站部署上线,支持注册登录,支持新建、编辑与搜索歌曲(第 8.4 节)
第三次课 11 月 21 日 辅导日:无新讲授内容 你依据讲义第 9 章(阶段 7–11)动手实践;老师现场辅导 角色权限、上传歌谱、打印、实时评论完成,在线展示网站(第 9.3 节)

1. 怎么用这份讲义

1.1 符号说明

符号 意思
灰色框里的内容 需原样复制、粘贴到终端或文件里的命令或代码
【中括号里的中文】 需换成实际信息,比如【你的项目ID】。粘贴后记得把整个【…】(连同中括号一起)替换掉
✅ 检查点 完成当前步骤后应看到的结果。确认符合预期后再进行下一步
❌ 如果失败 常见错误原因与解决方法
🤖 AI 提示词 可直接复制并发送给 AI 工具(ChatGPT / Codex / Claude Code / Cursor / Grok Bot)的提示语
🆘 卡住了 通用求助提示词,详见 1.3

1.2 两条路线,任选一条

  • 路线 A:跟着 AI 做(推荐体验)。 每一步将「🤖 AI 提示词」发送给 AI 工具,由 AI 生成代码。
  • 路线 B:直接复制参考代码(最稳妥)。 每一步均提供完整文件内容,所有代码汇总在 附录 B。如果 AI 生成的代码无法运行,直接使用讲义中的参考代码覆盖即可。

💡 两条路线可结合使用:先使用参考代码运行基础网站,再使用 AI 工具进行修改与功能扩展。

1.3 🆘 万能「我卡住了」提示词

遇到报错时,将下面这段话发送给 AI,并将报错信息完整贴在后面:

给 AI 的参考资料(不用看)
text
我没有编程经验,正在按照课程讲义做一个 Next.js + Cloudflare + Firebase 的歌谱网站。
我的电脑是【Windows 11 / macOS(Apple 芯片)/ macOS(Intel 芯片)/ Linux】。
我正在做的步骤是:【例如:阶段 4 第 3 步,创建 D1 数据库】
我输入的命令是:【把命令粘贴在这里】
完整的报错信息是:
【把终端里的全部红字/报错粘贴在这里】
请你:
1. 用简单的中文解释这个错误是什么意思;
2. 一步一步告诉我怎么修,每一步只给一个命令,并告诉我执行后应该看到什么;
3. 不要让我删除整个项目,也不要让我执行我看不懂的危险命令。

English version (if your AI tool works better in English):

给 AI 的参考资料(不用看)
text
I have no programming experience and am following a course to build a Next.js + Cloudflare + Firebase song sheet app.
My computer: [Windows 11 / macOS Apple Silicon / macOS Intel / Linux].
Step I'm on: [paste step]. Command I ran: [paste command].
Full error output:
[paste the whole error]
Please explain the error in simple words, then give me step-by-step fixes, one command at a time,
and tell me what I should see after each one. Don't ask me to delete the project.

1.4 三条安全规矩

  1. 密码、API Key、Token 切勿公开或截图分享。 Firebase 的网页配置(apiKey 等)属于公开前端配置,不算密码;Cloudflare 与 GitHub 的登录凭证严禁泄露。
  2. 执行未掌握的命令前,先向 AI 确认该命令的具体作用及是否具有破坏性。
  3. 每完成一个阶段,使用 Git 提交并保存进度(讲义在各阶段末尾均提供保存命令)。

2. 我们要做出什么

2.1 功能清单

必做(MVP,三次课内完成)

功能 说明 在哪个阶段做
登录 / 注册 支持邮箱密码注册与 Google 账号登录 阶段 5
歌曲库 歌曲列表展示与搜索(支持按歌名、作者、标签、歌词检索) 阶段 6
新建 / 编辑歌曲 编辑歌名、作者、调号、速度、标签、歌词与和弦,支持双栏实时预览 阶段 6
历史版本 保存前自动备份旧版本至数据库 阶段 6
角色权限 区分主领 leader、编辑 editor、只读 viewer 权限 阶段 7
上传歌谱 支持上传图片(PNG/JPG/WEBP)与 PDF 文件并存储至 R2 阶段 8
打印 支持一键打印,自动隐藏操作按钮与评论区,采用 A4 排版并可选是否显示和弦 阶段 9
实时评论 + 谁在线 支持多人协同下的实时评论更新与当前在线用户展示 阶段 10

2.2 歌词和弦的写法(本项目约定)

歌词格式与教会现用的 ICC 歌词系统(PPT 播放和歌词控制网页,代码库 icc-lyric-api)一致,在本系统录入的歌曲以后可以直接导入该系统。

给 AI 的参考资料(不用看)
text
# V1 V2;
@V1;
[G]奇异恩[G7]典 何等[C]甘[G]甜
我罪已得[Em]赦[D]免;
[G]前我失[G7]丧 今被[C]寻[G]回
瞎眼今得[D]看[G]见;
@V2;
...;
写法 含义
;(英文分号) 结束一屏。两个分号之间的内容是投影上的一屏,播放时点一下切换一屏
@V1 单独成一组,表示段落名。常用 V1、V2(主歌)、C(副歌)、B(桥段)、Tag(结尾)
# V1 V2 C 单独成一组,表示演唱顺序,只供同工参考,不投到屏幕上;名称与 @ 段落名对应
[G] 和弦,写在英文方括号内,置于对应文字前。[C]甘 表示 C 和弦显示在「甘」字正上方
==歌名、=副标题 可选。以 == 开头的一行是大标题,以 = 开头的一行是副标题
  • 必须使用英文分号 ;,中文分号 ; 不会分屏。
  • @、# 只在一组的开头才有特殊含义;一屏内的换行原样显示。
  • ICC 系统不显示和弦。src/lib/song-format.ts 中的 toIccLyric() 会去掉和弦,生成 ICC「歌词管理 → 添加歌词」所需的歌名、歌词和语言;ICC 的歌名以首字拼音首字母开头,例如 Q_奇异恩典。

3. 系统怎么拼在一起(了解即可)

网站跑在 Cloudflare 上,登录用 Firebase,代码放在 GitHub。细节交给 AI;你只要会打开控制台和复制提示词。

4. 数据放哪里(了解即可)

歌曲等结构化数据在 D1,歌谱文件在 R2,评论在 Firebase。建库与建表步骤里会让 AI 按参考资料写入。

5. 课前准备(账号最好第一次课前注册好,环境最晚第二次课前装好)

5.1 准备一台电脑

  • Windows 10 / 11、macOS(Apple 芯片 M1/M2/M3/M4 或 Intel 芯片)、Linux(如 Ubuntu)均可。
  • 至少 8 GB 内存,并留出 10 GB 以上空闲硬盘空间。
  • 需要拥有电脑的管理员权限(能自主安装软件)。公司电脑通常有软件安装限制或防火墙拦截,建议携带个人电脑。
  • 请携带电脑充电器。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我准备参加课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase)的学习,电脑是【我的系统】。
现在做「5.1 准备一台电脑」:检查并确认当前电脑的软硬件配置是否满足课程要求。
请指导我在【我的系统】中查看操作系统版本、内存大小、可用硬盘空间并确认是否具备管理员权限。
完成标准(✅):确认当前电脑系统满足要求,内存不少于 8 GB,空闲硬盘空间在 10 GB 以上,并拥有管理员权限。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

5.2 注册这些账号(全部免费,AI 订阅除外)

# 账号 网址 注意
1 AI 工具订阅(任选一种) ChatGPT https://chatgpt.com / Cursor https://cursor.com / Claude https://claude.ai 海报要求提前购买任一订阅
2 GitHub https://github.com/signup 请记住用户名,后续会用到
3 Cloudflare https://dash.cloudflare.com/sign-up 注册后需在邮箱中点击验证链接
4 Google 账号 https://accounts.google.com Firebase 需要用 Google 账号登录;已有 Gmail 则无需重复注册

💡 建议四个账号使用同一邮箱注册,便于记忆与管理。密码请妥善保存在密码管理器或浏览器中,切勿发给他人。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我准备参加课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase)的学习,电脑是【我的系统】。
现在做「5.2 注册这些账号」:依次注册并准备好课程全程所需的四个账号。
讲义要求的账号包括:1. AI 工具订阅(ChatGPT / Cursor / Claude 任选一种);2. GitHub(https://github.com/signup);3. Cloudflare(https://dash.cloudflare.com/sign-up,需点击邮箱验证链接);4. Google 账号(https://accounts.google.com,供 Firebase 使用)。
建议四个账号统一使用同一邮箱注册,请指导我逐项完成并确认。
完成标准(✅):四个账号全部完成注册或订阅,Cloudflare 邮箱验证链接已点击确认,密码已安全妥善保存。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

5.3 安装开发环境

请对照 第 6 章 依次完成 E1–E8。全部装好后,运行 6.9 的「总检查」,并截图留存。

第一次课只讲课和演示,不安排现场安装。 建议在第一次课前装好,最晚需在第二次课前装好。第二次课开场的半小时(里程碑 M0)专门用于集中检查和补装环境。


6. 环境搭建(分系统,一步一步来)

每一步均包含:操作内容 → 可直接复制的命令 → ✅ 预期结果 → ❌ 失败处理 → 🤖 AI 提示词。 不同操作系统的命令不同,只需查看对应系统的内容。

6.0 先学会打开「终端」

「终端」(Terminal)是输入命令与系统交互的窗口。

系统 怎么打开
Windows 10/11 按键盘上的 Win 键,输入 PowerShell,点击「Windows PowerShell」。Windows 11 也可以用「终端」(Terminal)。需要管理员权限时:右键点击并选择「以管理员身份运行」
macOS 按 Command + 空格,输入 Terminal(终端),按回车
Linux(Ubuntu) 按 Ctrl + Alt + T
  • 复制命令后,在终端中粘贴:Windows 使用鼠标右键或 Ctrl + V,macOS 使用 Command + V,Linux 使用 Ctrl + Shift + V。粘贴后按回车执行。
  • 重要提醒:每安装完一个软件,都必须关闭终端窗口并重新打开,使环境变量生效(刷新 PATH)。

🤖 AI 提示词:

text
我没有编程经验,我的电脑是【Windows 11 / macOS(Apple 芯片)/ macOS(Intel 芯片)/ Ubuntu】。
请一步一步教我怎么打开终端(命令行窗口),怎么在里面粘贴和执行命令,以及怎么判断命令执行成功还是失败。

6.1 E1:Windows 用户先做的两件事(Mac / Linux 跳过)

第 1 步:允许 PowerShell 运行脚本。 Windows 默认禁止运行脚本。如果不解除该限制,后续运行 npm、npx 时会报错「因为在此系统上禁止运行脚本」。

🎯 目标:允许 PowerShell 运行脚本。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

打开 PowerShell(普通模式即可),运行以下命令:

出现提示时,输入 Y 并按回车。

给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

✅ 检查:输入 Get-ExecutionPolicy -Scope CurrentUser,应显示 RemoteSigned。

❌ 如果提示「被组策略覆盖」:说明这是公司电脑的安全限制,建议改用个人电脑,或联系公司 IT 人员协助解除。:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

第 2 步:确认系统中已安装 winget(Windows 自带的软件包管理器)。

🎯 目标:确认系统中已安装 winget(Windows 自带的软件包管理器)。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词:

text
我用的是 Windows【10 / 11】,我要准备 Node.js 开发环境。
请教我:1)怎么用 PowerShell 设置执行策略为 RemoteSigned(只对当前用户);2)怎么检查电脑上有没有 winget,没有的话怎么安装。
每一步告诉我应该看到什么结果。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
powershell
winget --version

✅ 应显示一个版本号,例如 v1.x.x。

❌ 如果提示「无法识别 winget」:打开「Microsoft Store」(微软应用商店),搜索「应用安装程序」(App Installer),点击「更新」或「安装」。安装完成后重新打开 PowerShell。:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

6.2 E2:macOS 用户先装 Homebrew(Windows / Linux 跳过)

Homebrew 是 macOS 上的软件包管理器。

第 1 步: 安装 Apple 官方命令行工具。在终端运行:

🎯 目标:。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

在弹出窗口中点击「安装」,等待安装完成(约需 5–15 分钟)。若终端提示「already installed」,说明已经安装,直接跳到下一步即可。

给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
xcode-select --install

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 2 步: 安装 Homebrew(命令来自 https://brew.sh ):

🎯 目标:。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

安装过程中会提示输入电脑开机密码(注意:输入密码时屏幕不会显示任何字符或星号,直接输入后按回车即可)。

第 3 步(仅 Apple 芯片 M1/M2/M3/M4 需要做): 安装结束时,终端最后会显示一段「Next steps」提示,要求运行后续命令。请按终端提示复制运行,通常为以下两行:

查看 Mac 芯片类型:点击屏幕左上角苹果图标 →「关于本机」。若显示「芯片 Apple M…」即为 Apple 芯片;若显示「处理器 Intel…」即为 Intel 芯片。 Apple 芯片的 Homebrew 默认安装在 /opt/homebrew,Intel 芯片默认安装在 /usr/local。

🤖 AI 提示词:

text
我的 Mac 是【Apple 芯片 M1/M2/M3/M4 / Intel 芯片】,macOS 版本是【在「关于本机」里看到的版本】。
请一步一步教我安装 Xcode 命令行工具和 Homebrew,并告诉我安装完后怎么把 brew 加入 PATH,最后怎么验证安装成功。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
bash
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"

✅ 检查:关闭终端并重新打开,输入 brew --version,应显示类似 Homebrew 4.x.x 的版本号。

❌ 如果显示 command not found: brew:Apple 芯片用户请将上述第 3 步的两行命令再运行一次,然后重新打开终端。:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

6.3 E3:安装 Git(保存代码的工具)

系统 命令
Windows winget install --id Git.Git -e --source winget
macOS brew install git
Linux(Ubuntu/Debian) sudo apt update && sudo apt install -y git

安装完成后,关闭终端并重新打开,设置用户名和邮箱(请使用注册 GitHub 时的邮箱):

给 AI 的参考资料(不用看)
bash
git config --global user.name "【你的名字或英文名】"
git config --global user.email "【你注册 GitHub 用的邮箱】"

Windows 用户请再运行两行(用于处理换行符和长路径问题):

给 AI 的参考资料(不用看)
powershell
git config --global core.autocrlf true
git config --global core.longpaths true

Mac / Linux 用户请运行这一行:

给 AI 的参考资料(不用看)
bash
git config --global core.autocrlf input

✅ 检查:

给 AI 的参考资料(不用看)
bash
git --version

应显示 git version 2.xx.x。

❌ 常见问题:

  • git 不是内部或外部命令 / command not found: git:终端尚未重启。请关闭所有终端窗口并重新打开;若仍未解决,重启电脑。
  • Windows 安装时若 winget 询问「是否同意源协议」:输入 Y 并按回车。

🤖 AI 提示词:

text
我的电脑是【Windows 11 / macOS(Apple 芯片)/ macOS(Intel 芯片)/ Ubuntu】。
请一步一步教我安装 Git:Windows 用 winget,Mac 用 Homebrew,Linux 用 apt。
然后教我设置 user.name、user.email,以及适合我系统的换行符设置(core.autocrlf),最后用 git --version 验证。

🆘 卡住了:

text
我在【我的系统】上安装 Git,运行了【命令】,但是出现了下面的问题:
【粘贴完整报错或描述现象,例如:输入 git --version 提示找不到命令】
请帮我判断是没装上、PATH 没刷新,还是别的原因,并一步一步告诉我怎么修。

6.4 E4:安装 Node.js LTS(运行网站代码的引擎)

统一使用 LTS(长期支持)版本,稳定性最高。编写这份讲义时(2026 年 11 月)的 LTS 版本是 Node.js 26,具体请以 https://nodejs.org 官网显示的 LTS 版本为准。

Windows(二选一,推荐方案 A)

方案 A:直接使用 winget 安装:

给 AI 的参考资料(不用看)
powershell
winget install --id OpenJS.NodeJS.LTS -e --source winget

方案 B:使用 nvm-windows 安装(便于切换不同 Node 版本):

  1. 先卸载电脑上原有的 Node.js(在「设置 → 应用」中搜索 Node.js 并卸载),避免版本冲突。
  2. 安装 nvm-windows:winget install --id CoreyButler.NVMforWindows -e --source winget
  3. 以管理员身份重新打开 PowerShell,运行:
给 AI 的参考资料(不用看)
powershell
nvm install lts
nvm use lts

macOS(二选一,推荐方案 A)

方案 A:使用 Homebrew 安装:

给 AI 的参考资料(不用看)
bash
brew install node

brew install node 默认安装最新版本,通常即当前的 LTS 或更新版本。若需指定安装 LTS 版本,请使用方案 B。

方案 B:使用 nvm 安装:

给 AI 的参考资料(不用看)
bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash

关闭终端并重新打开,运行:

给 AI 的参考资料(不用看)
bash
nvm install --lts

Linux(推荐使用 nvm):运行与上述 macOS 方案 B 相同的两条命令即可。

✅ 检查(关闭终端重新打开后):

给 AI 的参考资料(不用看)
bash
node -v
npm -v

应分别显示 v26.x.x(或更新的 LTS 版本号,至少要在 v20 以上)和 11.x.x 等版本号。

❌ 常见问题:

  • node 不是内部或外部命令:先关闭并重新打开终端;若仍未解决,重启电脑。
  • Windows 运行 npm 报错「因为在此系统上禁止运行脚本」:请返回 6.1 第 1 步设置执行策略。
  • nvm-windows 安装后运行 node -v 仍显示旧版本:旧版本 Node 未卸载干净。可运行 nvm debug 查看原因,卸载旧版 Node 后再运行 nvm use lts。
  • Mac 提示 nvm: command not found:先重新打开终端;若仍未解决,运行 source ~/.zshrc(Linux 用户运行 source ~/.bashrc)。

🤖 AI 提示词:

text
我的电脑是【Windows 11 / macOS(Apple 芯片)/ macOS(Intel 芯片)/ Ubuntu】,我没有编程经验。
请教我安装 Node.js 的 LTS 版本:Windows 用 winget 安装 OpenJS.NodeJS.LTS;Mac 用 Homebrew 或 nvm;Linux 用 nvm。
装完教我用 node -v 和 npm -v 验证,并告诉我版本号大概长什么样才算对。

🆘 卡住了:

text
我在【我的系统】上安装 Node.js,用的方法是【winget / nvm-windows / Homebrew / nvm】。
运行【node -v 或其他命令】时出现:
【粘贴完整输出】
请帮我判断:是没装上、PATH 没刷新、旧版本冲突,还是 PowerShell 执行策略问题?一步一步告诉我怎么修。

6.5 E5:npm(装代码「零件」的工具)

  • npm 已随 Node.js 自动安装,课程全程使用 npm,无需安装其他工具。

✅ 检查:运行 npm -v 能正常显示版本号即可。

❌ 若运行 npm install 速度过慢或卡住:通常为网络或代理问题,请参考 6.10「代理和防火墙」。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「6.5 E5:npm」:确认 npm 已随 Node.js 一起装好、可以使用。
请带我运行检查命令:npm -v。
完成标准(✅):运行 npm -v 能正常显示版本号即可。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

6.6 E6:安装代码编辑器(VS Code 或 Cursor,二选一)

系统 VS Code Cursor
Windows winget install --id Microsoft.VisualStudioCode -e --source winget winget install --id Anysphere.Cursor -e --source winget
macOS brew install --cask visual-studio-code brew install --cask cursor
Linux 到 https://code.visualstudio.com/download 下载 .deb 安装 到 https://cursor.com/downloads 下载

也可以直接前往官网下载安装包:VS Code https://code.visualstudio.com ,Cursor https://cursor.com 。

  • 订阅 Cursor 请直接使用 Cursor(内置 AI 助手)。
  • 使用 ChatGPT(Codex)或 Claude Code,安装 VS Code 即可,并参照各自官网说明安装 Codex 或 Claude Code(详见附录 C 链接)。

✅ 检查:能够正常打开编辑器,并可在编辑器中打开终端:点击顶部菜单「终端 → 新建终端」(Terminal → New Terminal)。后续所有命令均可直接在编辑器内置终端中运行。

🤖 AI 提示词:

text
我的电脑是【我的系统】,我买了【ChatGPT / Claude / Cursor】的订阅。
请教我安装合适的代码编辑器(VS Code 或 Cursor),以及怎么把我买的 AI 编程工具(Codex / Claude Code / Cursor)装好、登录好,
最后教我在编辑器里打开内置终端。

6.7 E7:登录 GitHub(让电脑可以上传代码)

使用 GitHub 官方命令行工具 gh,可直接登录并通过命令行创建代码仓库。

第 1 步:安装 gh

🎯 目标:安装 gh。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

系统 命令
Windows winget install --id GitHub.cli -e --source winget
macOS brew install gh
Linux(Ubuntu) 按 https://github.com/cli/cli/blob/trunk/docs/install_linux.md 的 apt 步骤安装

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 2 步:关闭终端并重新打开,运行登录命令:

🎯 目标:关闭终端并重新打开,运行登录命令:。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

依次进行如下选择(使用方向键移动,按回车确认):

  1. Where do you use GitHub? → 选 GitHub.com
  2. What is your preferred protocol for Git operations? → 选 HTTPS
  3. Authenticate Git with your GitHub credentials? → 选 Yes
  4. How would you like to authenticate GitHub CLI? → 选 Login with a web browser
  5. 终端会显示 8 位一次性验证码(例如 ABCD-1234)。复制该验证码后按回车,浏览器将自动打开验证页面,粘贴验证码并点击「Authorize」(授权)。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
gh auth login

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 3 步:配置 Git 自动使用 gh 的登录凭证:

🎯 目标:配置 Git 自动使用 gh 的登录凭证:。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

应显示 ✓ Logged in to github.com account 【你的用户名】。

🤖 AI 提示词:

text
我的电脑是【我的系统】,已经装好了 Git。请教我安装 GitHub CLI(gh),
用 gh auth login 通过浏览器登录 GitHub(HTTPS 方式),再运行 gh auth setup-git,最后用 gh auth status 确认登录成功。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)

🆘 卡住了:

text
我在【我的系统】上用 gh auth login 登录 GitHub,在【哪一步】卡住了,终端显示:
【粘贴输出】
请告诉我怎么继续,或者怎么改用 Git Credential Manager 登录。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
gh auth setup-git
bash
gh auth status

✅ 检查:

❌ 常见问题:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

6.8 E8:登录 Cloudflare

本课程使用 Cloudflare 官方命令行工具 cf(2026 年 9 月发布,目前为测试版)管理账号、数据库和存储。为避免上课期间版本变化,安装时固定版本号。

第 1 步:安装 cf(Windows 在 PowerShell 中运行,Mac 在终端中运行,命令相同):

🎯 目标:安装 cf。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

应显示 1.0.0-beta.27。

给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm install --global cf@1.0.0-beta.27
bash
cf --version

✅ 检查:

❌ 如果失败:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

第 2 步:登录 Cloudflare 账号。

🎯 目标:登录 Cloudflare 账号。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

浏览器将自动打开 Cloudflare 授权页面,点击「Allow」(允许),看到授权成功后回到终端。

给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
cf auth login

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 3 步:确认登录状态。

🎯 目标:确认登录状态。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

部署网站时,OpenNext 部署工具会在后台调用 Cloudflare 的另一个工具 Wrangler。Wrangler 无需安装,但需要单独登录一次,在阶段 3 第 1 步完成。

🤖 AI 提示词:

text
我的电脑是【我的系统】,已经装好 Node.js。请教我用 npm install --global cf@1.0.0-beta.27 安装 Cloudflare 的 cf 命令行工具,
用 cf --version 确认版本,再用 cf auth login 登录 Cloudflare,最后用 cf auth whoami 确认登录成功。如果安装后提示找不到 cf 命令,或浏览器授权后终端卡住,应该怎么办?

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)

🆘 求助提示词:

text
我在安装或登录 Cloudflare 的 cf 工具时出错。我的系统是【我的系统】,运行的命令是【运行的命令】,完整报错如下:
【粘贴报错】
请告诉我原因和下一步要运行的命令。我没有编程经验,请一步一步说明。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
cf auth whoami

✅ 检查:显示 "authenticated": true 和你注册 Cloudflare 时使用的邮箱。

❌ 常见问题:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

6.9 ✅ 环境总检查

关闭所有终端窗口,重新打开一个终端,依次运行以下命令:

给 AI 的参考资料(不用看)
bash
git --version
node -v
npm -v
gh auth status
cf --version
cf auth whoami
命令 应该看到
git --version git version 2.…
node -v v26.…(或更新的 LTS,至少 v20)
npm -v 一个版本号,如 11.…
gh auth status Logged in to github.com
cf --version 1.0.0-beta.27
cf auth whoami "authenticated": true 和你的 Cloudflare 邮箱

若以上各项均显示正常,开发环境即配置完成。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「6.9 环境总检查」:在终端依次运行环境检查命令,确认全部开发工具配置正确。
请带我依次运行以下命令并核对输出:git --version、node -v、npm -v、gh auth status、cf --version、cf auth whoami。
完成标准(✅):git 显示 git version 2.…;node 显示 v26.…(至少 v20);npm 显示版本号;gh 显示 Logged in to github.com;cf --version 显示 1.0.0-beta.27;cf auth whoami 显示 "authenticated": true 和我的 Cloudflare 邮箱。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

6.10 各系统常见坑(遇到再看)

问题 现象 解决
PowerShell 执行策略 无法加载文件 …npm.ps1,因为在此系统上禁止运行脚本 运行 Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
PATH 未刷新 刚装完软件,却提示「不是内部或外部命令」/ command not found 关闭所有终端和编辑器窗口后重新打开;若仍未解决,重启电脑
公司代理 / 防火墙 npm install 卡住,报错 ETIMEDOUT、ECONNRESET、SELF_SIGNED_CERT_IN_CHAIN;或 cf auth login 无法打开 建议切换至手机热点或家庭网络。若必须使用公司网络,向公司 IT 获取代理地址,再参考下方 AI 提示词配置
Apple 芯片 vs Intel 提示 brew 找不到,或安装了 Intel 版软件导致运行缓慢 按 6.2 第 3 步把 /opt/homebrew/bin 加到 PATH;下载软件安装包时认准「Apple Silicon / arm64」版本
Windows 换行符 Git 提示 LF will be replaced by CRLF 属于提示信息,非错误。若已按 6.3 设置 core.autocrlf true,可忽略该提示
Windows 长路径 报错 Filename too long / 路径太长 运行 git config --global core.longpaths true;并将项目存放在简短路径下,例如 C:\code\
项目放在 OneDrive / iCloud 同步文件夹 依赖安装缓慢、文件被锁定或报错 将项目移动到非云同步本地目录,例如 Windows 存放在 C:\code,Mac 存放在 ~/code
WSL 是否需要安装 WSL 无需安装。若已使用 WSL,请将项目放在 WSL 内部的 ~/ 目录下
杀毒软件 安装软件时被拦截,或运行 npm install 速度过慢 允许程序运行,或将项目所在目录加入杀毒软件白名单

🤖 代理问题提示词:

text
我在公司网络下,npm install / cf auth login / npx wrangler login 连不上,报错是:
【粘贴报错】
公司 IT 给我的代理地址是【代理地址,例如 http://proxy.company.com:8080】,我的系统是【我的系统】。
请教我怎么给 npm、git 和终端设置代理,以及用完后怎么取消代理。

7. 第一次课(11 月 7 日):讲课日——老师讲完全部内容 + 完整演示

7.1 本次目标

  • 理解 AI 编程助手的功能,掌握编写清晰指令的方法。
  • 介绍项目涉及的所有概念:GitHub 存档、Next.js、部署到 Cloudflare、D1、R2、Firebase 登录、角色权限、打印、实时评论和「谁在线」。
  • 演示完整开发流程:从空目录起步,按阶段 1–11 搭建支持登录、歌谱编辑、打印与实时评论的线上网站。
  • 解析系统架构图,明确各云服务的分工职责。
  • 说明第二、三次课的安排:每日里程碑目标、遇阻求助流程与课后环境配置要求。

📌 第一次课你不需要同步输入命令,以理解流程为主。开发环境(第 6 章)请在第一次课之后、第二次课之前在个人电脑完成配置;第二次课开场会有半小时统一检查环境。

7.4 ✅ 本次课结束时学员应该做到

  • 能够说明各服务分工:D1 存储内容、R2 存储内容、Firebase 负责功能。
  • 了解从空目录到完整上线的全流程,明确阶段 1–11 的目标。
  • 明确后续课程目标,掌握使用 1.3 🆘提示词排查问题的方法。
  • 明确课后准备事项(5.2 四个账号注册、第 6 章环境配置)。

7.5 作业(第二次课前完成)

  1. 注册 5.2 要求的四个账号。
  2. 完成第 6 章开发环境搭建,确保 6.9 总检查全部通过并截图保存。遇到问题可使用各步骤的 🤖 提示词或 1.3 的🆘提示词向 AI 提问;未完成的部分可在第二次课开场补装。

8. 第二次课(11 月 14 日):辅导日——自己动手:环境、部署上线、数据库、登录、歌曲编辑器

8.1 本次目标

完成本次课程后,将构建并上线一套歌谱系统:支持用户注册登录、新建歌曲、编辑歌词与和弦、搜索歌曲,并具备历史版本自动归档功能。

8.2 今天的里程碑(⏱ 参考时间,可按现场调整)

参考时间 里程碑 做哪些步骤 ✅ 怎么算完成(自己核对)
14:00 – 14:30 M0 环境就绪 运行 6.9 环境总检查;未完成项按第 6 章补装;确认 5.2 四个账号均可登录 6.9 总检查全部通过(请准备好截图)
14:30 – 14:50 M1 本地跑起来 阶段 1 阶段 1 的 ✅ 检查点:浏览器打开 http://localhost:3000 显示 Next.js 欢迎页面
14:50 – 15:05 M2 第一个存档 阶段 2 阶段 2 的 ✅ 检查点:GitHub 仓库页面显示已提交代码
15:05 – 15:40 M3 第一次部署上线 阶段 3 阶段 3 的 ✅ 检查点:手机可访问 workers.dev 公开网址
15:40 – 15:55 休息
15:55 – 16:15 M4 D1 数据库 阶段 4 阶段 4 的 ✅ 检查点:线上数据库查询到导入的歌曲数据
16:15 – 16:55 M5 能登录 阶段 5 阶段 5 的 ✅ 检查点:在本地 http://localhost:3000 注册后,右上角显示 你的名字(leader)(线上登录需在阶段 6 第 3、4 步部署后生效)
16:55 – 17:40 M6 歌曲编辑器上线 阶段 6(时间受限时优先确保第 2 步本地测试通过,第 3–4 步部署可顺延至 17:40 后或课后完成) 阶段 6 的 ✅ 检查点:线上网址支持新建、编辑、搜索歌曲,并已完成代码存档
17:40 – 18:00 互相检查 + 收尾 互相访问对方的线上网址测试;未完成的记录当前进度步骤 对照 8.4

若课堂进度落后,课后可依据讲义步骤继续完成。**优先保证达成 M3(第一次部署上线)和 M5(登录)**两个关键里程碑。

遇阻排查流程:① 查看当前步骤的「❌ 如果失败」说明;② 将 1.3 的🆘提示词及完整错误日志发送给 AI;③ 若超过 10 分钟未解决,贴红色便利贴求助。

8.3 开始前:一个重要习惯

所有命令都必须在项目文件夹内运行。 阶段 1 创建项目后,使用 VS Code / Cursor 打开 worship-songs 文件夹,点击顶部菜单「终端 → 新建终端」,终端工作目录即为项目文件夹。

💡 npm run dev 运行期间会持续占用终端窗口。若需执行其他命令(如 Git 存档、数据库命令),再次点击顶部菜单「终端 → 新建终端」,在新建的终端窗口中输入命令,保持原服务继续运行。

确认当前工作目录的方法:在终端中输入:

  • Windows:输入 pwd,应显示 C:\code\worship-songs
  • Mac / Linux:输入 pwd,应显示 /Users/【你的用户名】/code/worship-songs(Linux 显示 /home/【你的用户名】/code/worship-songs)

新建文件: 在 VS Code / Cursor 左侧文件列表空白处右键单击 → 选择「新建文件」(New File),输入完整路径(例如 src/lib/server.ts)并按回车,编辑器会自动创建所需目录。将讲义代码粘贴至文件中,按 Ctrl + S(Mac 按 Command + S)保存。

覆盖已有文件: 打开目标文件,按 Ctrl + A(Mac 按 Command + A)全选原有内容,直接粘贴讲义中的完整代码并保存。


阶段 1:建项目,本地跑起来

第 1 步:新建代码目录并进入。

🎯 目标:新建代码目录并进入。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

若终端提示目录已存在,直接运行 cd 命令即可。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 1 第 1 步:新建代码目录并进入」:创建存放课程项目的代码目录并进入该目录。
请指导我运行下面「参考资料」或本提示中的命令:Windows 运行 mkdir C:\code 和 cd C:\code;Mac / Linux 运行 mkdir -p ~/code 和 cd ~/code。
完成标准(✅):成功创建代码目录并进入,当前终端工作路径为 C:\code(Mac/Linux 为 ~/code)。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)

Windows(PowerShell):

powershell
mkdir C:\code
cd C:\code

Mac / Linux:

bash
mkdir -p ~/code
cd ~/code

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 2 步:创建 Next.js 项目。(已验证版本,请原样复制执行)

🎯 目标:创建 Next.js 项目。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

首次运行若提示 Ok to proceed? (y),输入 y 并按回车。等待安装完成(约 1–3 分钟)。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 1 第 2 步:创建 Next.js 项目」:通过官方脚手架创建指定版本的 Next.js 项目。
请指导我原样运行下面的命令:npx create-next-app@16.4.5 worship-songs --ts --tailwind --eslint --app --src-dir --import-alias "@/*" --use-npm --yes(若提示 Ok to proceed? (y) 输入 y 回车)。
完成标准(✅):终端输出 Success! Created worship-songs at ...。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npx create-next-app@16.4.5 worship-songs --ts --tailwind --eslint --app --src-dir --import-alias "@/*" --use-npm --yes

✅ 检查点:终端输出 Success! Created worship-songs at ...。

❌ 如果失败:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

第 3 步:使用编辑器打开项目。

🎯 目标:使用编辑器打开项目。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

在 VS Code / Cursor 中点击菜单「文件 → 打开文件夹」(File → Open Folder),选择 C:\code\worship-songs(Mac 选择 ~/code/worship-songs)。打开后,点击顶部菜单「终端 → 新建终端」。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 1 第 3 步:使用编辑器打开项目」:在代码编辑器中打开项目文件夹并新建集成终端。
请指导我在 VS Code 或 Cursor 中通过菜单「文件 → 打开文件夹」选择 C:\code\worship-songs(Mac 选择 ~/code/worship-songs),并在编辑器内点击「终端 → 新建终端」。
完成标准(✅):在编辑器中成功打开 worship-songs 项目文件夹,并在编辑器内成功新建终端。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 4 步:启动本地开发服务器。

🎯 目标:启动本地开发服务器。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

验证完成后,在终端按 Ctrl + C 停止服务器(Mac 同样按 Ctrl + C)。执行其他命令前,需先按 Ctrl + C 终止当前服务器,或在新建终端窗口中执行。


🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 1 第 4 步:启动本地开发服务器」:启动本地开发服务器并在浏览器中验证欢迎页面。
请指导我在终端运行命令:npm run dev。测试完成后若需执行其他命令,指导我按 Ctrl + C 停止服务器。
完成标准(✅):终端显示 Local: http://localhost:3000。浏览器打开 http://localhost:3000 ,显示 Next.js 欢迎页面。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm run dev

✅ 检查点:终端显示 Local: http://localhost:3000。浏览器打开 http://localhost:3000 ,显示 Next.js 欢迎页面。

❌ 如果失败:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

阶段 2:上传到 GitHub(第一个存档)

create-next-app 创建项目时已在本地生成初始 Git 存档。执行以下命令将项目推送至 GitHub 私有仓库:

给 AI 的参考资料(不用看)
bash
gh repo create worship-songs --private --source=. --remote=origin --push

✅ 检查点:终端显示 ✓ Created repository 【你的GitHub用户名】/worship-songs on GitHub 和 ✓ Pushed commits。浏览器打开 https://github.com/【你的GitHub用户名】/worship-songs 可查看推送的代码。

❌ 如果失败:

  • 提示 gh: command not found 或不是内部或外部命令:参考 6.7 安装 gh,安装完成后重启终端。
  • 提示 Name already exists on this account:GitHub 账号下已有同名仓库。将上述命令中的 worship-songs 替换为 worship-songs-2 后重新执行。
  • 提示 not a git repository:依次执行以下三行命令,再重新执行上述推送命令:
给 AI 的参考资料(不用看)
bash
git init
git add -A
git commit -m "Initial commit"

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 2 上传到 GitHub(第一个存档)」:将项目推送到 GitHub 私有仓库进行首次代码存档。
请指导我运行推送命令:gh repo create worship-songs --private --source=. --remote=origin --push。若提示 not a git repository,则先执行 git init、git add -A、git commit -m "Initial commit" 再执行推送命令。
完成标准(✅):终端显示 ✓ Created repository 【你的GitHub用户名】/worship-songs on GitHub 和 ✓ Pushed commits。浏览器打开 https://github.com/【你的GitHub用户名】/worship-songs 可查看推送的代码。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

日常代码存档命令(在引号中注明本次修改内容):

给 AI 的参考资料(不用看)
bash
git add -A
git commit -m "【这次做了什么,例如:加了登录页】"
git push

阶段 3:接上 Cloudflare,第一次部署上线

第 1 步:确认 Cloudflare 登录状态,并为部署工具登录。

🎯 目标:确认 Cloudflare 登录状态,并为部署工具登录。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

npm run deploy 由 OpenNext 在后台调用 Wrangler 完成部署。Wrangler 无需安装,但不共用 cf 的登录,所以需要在这里单独登录一次。之后的部署不再需要登录。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 3 第 1 步:确认 Cloudflare 登录状态,并为部署工具登录」:确认 cf CLI 登录状态并为 wrangler 部署工具完成独立登录。
请带我依次运行命令:cf auth whoami 以及 npx wrangler login。在 wrangler 打开的浏览器授权页面中点击「Allow」。
完成标准(✅):cf auth whoami 显示 "authenticated": true;运行 npx wrangler login 后浏览器打开授权页面,点击「Allow」,终端显示 Successfully logged in。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
cf auth whoami
npx wrangler login

✅ cf auth whoami 显示 "authenticated": true;运行 npx wrangler login 后浏览器打开授权页面,点击「Allow」,终端显示 Successfully logged in。首次运行若提示 Ok to proceed? (y),输入 y 并按回车。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 2 步:运行 OpenNext 自动配置工具。

🎯 目标:运行 OpenNext 自动配置工具。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 3 第 2 步:运行 OpenNext 自动配置工具」:运行 OpenNext 迁移工具为 Next.js 自动生成 Cloudflare 适配配置。
请指导我运行命令:npx @opennextjs/cloudflare migrate。首次若提示 Ok to proceed? (y) 输入 y 回车,其他选项直接回车使用默认配置,忽略 R2 cache 相关警告。
完成标准(✅):项目根目录生成 wrangler.jsonc 和 open-next.config.ts 文件。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npx @opennextjs/cloudflare migrate

✅ 检查点:项目根目录生成 wrangler.jsonc 和 open-next.config.ts 文件。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 3 步:覆盖 next.config.ts 配置。

🎯 目标:覆盖 next.config.ts 配置。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

create-next-app 生成的默认配置包含实验性功能,会导致后续部署报错,需替换为以下稳定版本。

打开 next.config.ts,全选内容并替换为:

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 3 第 3 步:覆盖 next.config.ts」:把默认配置换成讲义的稳定版本,避免后续部署报错。
下面「参考资料」里有完整代码,请原样写入 next.config.ts(不要改动内容),写完告诉我怎么确认保存成功。
完成标准(✅):next.config.ts 的内容与讲义完全一致(空的 nextConfig,末尾调用 initOpenNextCloudflareForDev()),已保存。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
ts
import type { NextConfig } from "next";
    

const nextConfig: NextConfig = {};

export default nextConfig;

// Lets npm run dev use local D1 / R2 (Cloudflare bindings). import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"; initOpenNextCloudflareForDev();</code></pre></div>

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 4 步:在本地进行 Cloudflare 模式预览。

🎯 目标:在本地进行 Cloudflare 模式预览。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

等待构建完成(约 1–2 分钟),终端将显示本地预览地址(通常为 http://localhost:8787)。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 3 第 4 步:在本地进行 Cloudflare 模式预览」:在本地以 Cloudflare 模式构建并预览项目效果。
请指导我运行命令:npm run preview。等待构建完成后在浏览器访问本地预览地址(通常为 http://localhost:8787),确认后按 Ctrl + C 停止预览。
完成标准(✅):浏览器打开该地址,显示阶段 1 相同的欢迎页面。确认正常后在终端按 Ctrl + C 停止预览。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm run preview

✅ 检查点:浏览器打开该地址,显示阶段 1 相同的欢迎页面。确认正常后在终端按 Ctrl + C 停止预览。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 5 步:部署上线。

🎯 目标:部署上线。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  • 若 Cloudflare 账号首次部署 Workers,终端将提示配置 workers.dev 子域名,输入英文字符(即文中的【你的子域名】)。

记录该网址备用。下文中统一指代为【你的网址】。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 3 第 5 步:部署上线」:将项目通过 OpenNext 首次正式部署到 Cloudflare Workers。
请指导我运行命令:npm run deploy。若提示配置 workers.dev 子域名,请输入英文字符【你的子域名】。
完成标准(✅):终端输出公开网址 https://worship-songs.【你的子域名】.workers.dev。使用手机访问该网址,可正常打开欢迎页面。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm run deploy

✅ 检查点:终端输出公开网址 https://worship-songs.【你的子域名】.workers.dev。使用手机访问该网址,可正常打开欢迎页面。

❌ 如果失败:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

第 6 步:存档。

🎯 目标:存档。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):


🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 3 第 6 步:存档」:将完成初次部署的代码变更提交并推送到 GitHub 远程仓库。
请指导我依次运行这些 Git 命令:git add -A、git commit -m "Deploy to Cloudflare with OpenNext"、git push。
完成标准(✅):代码变更成功 commit 并 push 到 GitHub 远程仓库。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)

🤖 路线 A 提示词:

text
我有一个用 create-next-app 创建的 Next.js 项目,想用 OpenNext 适配器(@opennextjs/cloudflare)部署到 Cloudflare Workers。
我已经用 cf auth login 和 npx wrangler login 登录了 Cloudflare。请一步一步告诉我要运行的命令,并解释每个新生成的文件是做什么的。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
git add -A
git commit -m "Deploy to Cloudflare with OpenNext"
git push

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

阶段 4:建 D1 数据库

第 1 步:在 Cloudflare 上创建数据库(参数 apac 表示将数据库建在亚太地区):

🎯 目标:在 Cloudflare 上创建数据库。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

在项目根目录(worship-songs 文件夹)中运行:

复制 uuid 的值(不含引号)。这就是数据库的 database_id,下文统一写作【你的database_id】,第 2 步和第 5 步都会用到。

若未复制,可运行 cf d1 list --name worship-db 查看,其中的 uuid 即 database_id。 若账号下有多个 Cloudflare 账户,cf 会让你选择,用方向键选中后按回车。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 4 第 1 步:在 Cloudflare 上创建数据库」:在 Cloudflare 亚太地区创建名为 worship-db 的 D1 数据库并获取 uuid。
请指导我在项目根目录运行:cf d1 create --name worship-db --primary-location-hint apac。若未复制 uuid 可运行 cf d1 list --name worship-db 查看。
完成标准(✅):终端输出一段 JSON,其中包含 "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 和 "name": "worship-db",并成功记录下该 uuid 作为【你的database_id】。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
cf d1 create --name worship-db --primary-location-hint apac
text
"uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "worship-db",

✅ 检查点:终端输出一段 JSON,其中包含:

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 2 步:覆盖 wrangler.jsonc。 打开项目根目录下的 wrangler.jsonc,全选并替换为以下内容,然后将【替换成 cf d1 create 输出的 uuid】替换为刚才复制的 uuid(保留两端英文双引号):

🎯 目标:覆盖 wrangler.jsonc。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 4 第 2 步:覆盖 wrangler.jsonc」:在 wrangler.jsonc 中配置并绑定 D1 数据库。
下面「参考资料」里有完整代码,请原样写入 wrangler.jsonc(不要改动内容),写完告诉我怎么确认保存成功。
写完后请提醒我将其中的【替换成 cf d1 create 输出的 uuid】替换为刚才复制的【你的database_id】(保留两端双引号)。
完成标准(✅):wrangler.jsonc 文件成功覆盖保存,且 d1_databases 中的 database_id 已正确填入真实的 uuid。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "main": ".open-next/worker.js",
  "name": "worship-songs",
  "compatibility_date": "2026-11-04",
  "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
  "assets": {
    "directory": ".open-next/assets",
    "binding": "ASSETS"
  },
  "services": [
    {
      "binding": "WORKER_SELF_REFERENCE",
      "service": "worship-songs"
    }
  ],
  "images": {
    "binding": "IMAGES"
  },
  // ↓↓↓ 我们自己加的一段(D1 数据库) ↓↓↓
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "worship-db",
      "database_id": "【替换成 cf d1 create 输出的 uuid】"
    }
  ]
}

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 3 步:新建数据库结构文件 db/schema.sql(路径为 db/schema.sql):

🎯 目标:新建数据库结构文件 db/schema.sql。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 4 第 3 步:新建数据库结构文件 db/schema.sql」:创建定义数据表结构的 SQL 迁移文件。
下面「参考资料」里有完整代码,请原样写入 db/schema.sql(不要改动内容),写完告诉我怎么确认保存成功。
完成标准(✅):db/schema.sql 文件创建成功并保存完整的建表 SQL 内容。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
sql
-- 敬拜团歌谱系统 数据库结构 (Cloudflare D1 / SQLite)
CREATE TABLE IF NOT EXISTS users (
  id TEXT PRIMARY KEY,                 -- Firebase uid
  email TEXT NOT NULL,
  display_name TEXT,
  role TEXT NOT NULL DEFAULT 'viewer' CHECK (role IN ('leader', 'editor', 'viewer')),
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
    

CREATE TABLE IF NOT EXISTS songs ( id TEXT PRIMARY KEY, title TEXT NOT NULL, artist TEXT, song_key TEXT, -- e.g. G, D, Eb tempo INTEGER, -- BPM tags TEXT, -- comma separated content TEXT NOT NULL DEFAULT '', -- lyrics + chords, see format guide created_by TEXT, updated_by TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) );

CREATE TABLE IF NOT EXISTS song_versions ( id INTEGER PRIMARY KEY AUTOINCREMENT, song_id TEXT NOT NULL REFERENCES songs(id) ON DELETE CASCADE, title TEXT NOT NULL, content TEXT NOT NULL, edited_by TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) );

CREATE TABLE IF NOT EXISTS attachments ( id TEXT PRIMARY KEY, song_id TEXT NOT NULL REFERENCES songs(id) ON DELETE CASCADE, r2_key TEXT NOT NULL, file_name TEXT NOT NULL, content_type TEXT, size INTEGER, uploaded_by TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) );

CREATE INDEX IF NOT EXISTS idx_songs_title ON songs(title); CREATE INDEX IF NOT EXISTS idx_versions_song ON song_versions(song_id); CREATE INDEX IF NOT EXISTS idx_attachments_song ON attachments(song_id);</code></pre></div>

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 4 步:新建示例数据文件 db/seed.sql:

🎯 目标:新建示例数据文件 db/seed.sql:。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 4 第 4 步:新建示例数据文件 db/seed.sql」:创建包含示例歌曲「奇异恩典」的初始化数据文件。
下面「参考资料」里有完整代码,请原样写入 db/seed.sql(不要改动内容),写完告诉我怎么确认保存成功。
歌词遵循 ICC 格式约定(见讲义 2.2:英文分号 ; 分屏,@V1 段落名,# 演唱顺序,[G] 和弦)。
完成标准(✅):db/seed.sql 文件创建成功并保存示例歌曲数据。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
sql
INSERT OR IGNORE INTO songs (id, title, artist, song_key, tempo, tags, content) VALUES
('demo-amazing-grace', '奇异恩典 Amazing Grace', 'John Newton', 'G', 72, '经典,赞美',
'# V1 V2;
@V1;
[G]奇异恩[G7]典 何等[C]甘[G]甜
我罪已得[Em]赦[D]免;
[G]前我失[G7]丧 今被[C]寻[G]回
瞎眼今得[D]看[G]见;
@V2;
[G]如此恩[G7]典 使我[C]敬[G]畏
使我心得[Em]安[D]慰;
[G]初信之[G7]时 即蒙[C]恩[G]惠
真是何等[D]宝[G]贵;');

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 5 步:向本地数据库和线上数据库导入数据表与示例数据。

🎯 目标:向本地数据库和线上数据库导入数据表与示例数据。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

先导入本地数据库(供 npm run dev 使用):

本地数据库由 OpenNext 在后台调用 Wrangler 管理,保存在项目的 .wrangler 目录中,cf 无法写入,因此这两条使用 npx wrangler,无需登录。

再导入线上数据库(供部署后的网站使用)。cf 会按文件名顺序执行 db 目录下的 .sql 文件(先 schema.sql,后 seed.sql),并记录已执行的文件,重复运行不会重复导入:

💡 本地数据库和线上数据库相互独立:在一个里改数据,另一个不会变化。

输出中应包含 "title": "奇异恩典 Amazing Grace"。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 4 第 5 步:向本地数据库和线上数据库导入数据表与示例数据」:分别将结构与示例数据导入本地 D1 数据库与 Cloudflare 线上 D1 数据库。
请带我先运行本地导入命令:npx wrangler d1 execute worship-db --local --file=./db/schema.sql 和 npx wrangler d1 execute worship-db --local --file=./db/seed.sql;再运行线上导入命令:cf d1 migrations apply 【你的database_id】 --dir db;最后运行查询验证:cf d1 query 【你的database_id】 --sql "SELECT title FROM songs"。
完成标准(✅):cf d1 migrations apply 输出中 schema.sql 和 seed.sql 的 status 均为 ✅;运行 cf d1 query 【你的database_id】 --sql "SELECT title FROM songs" 输出中包含 "title": "奇异恩典 Amazing Grace"。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npx wrangler d1 execute worship-db --local --file=./db/schema.sql
npx wrangler d1 execute worship-db --local --file=./db/seed.sql
bash
cf d1 migrations apply 【你的database_id】 --dir db
bash
cf d1 query 【你的database_id】 --sql "SELECT title FROM songs"

✅ 检查点:查询线上数据库验证导入结果:

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 6 步:生成类型文件(生成 DB 数据库绑定的类型定义):

🎯 目标:生成类型文件。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 4 第 6 步:生成类型文件」:生成 Cloudflare D1 数据库绑定的 TypeScript 类型定义文件。
请指导我运行命令:npm run cf-typegen。
完成标准(✅):项目根目录下生成 cloudflare-env.d.ts 文件,其中包含 DB: D1Database。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm run cf-typegen

✅ 检查点:项目根目录下生成 cloudflare-env.d.ts 文件,其中包含 DB: D1Database。

❌ 如果失败:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

第 7 步:存档。

🎯 目标:存档。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):


🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 4 第 7 步:存档」:将 D1 数据库配置与 SQL 文件推送到 GitHub 远程仓库存档。
请指导我依次运行这些 Git 命令:git add -A、git commit -m "Add D1 database"、git push。
完成标准(✅):数据库相关代码变更成功 commit 并 push 到 GitHub 远程仓库。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)

🤖 路线 A 提示词:

text
我的 Next.js 项目用 @opennextjs/cloudflare 部署在 Cloudflare Workers。请帮我设计一个敬拜团歌谱系统的 Cloudflare D1(SQLite)数据库,
需要 4 张表:users(id 是 Firebase uid,role 只能是 leader/editor/viewer,默认 viewer)、songs(歌名、作者、调号 song_key、速度 tempo、标签 tags、歌词和弦 content、创建人、修改人、时间)、
song_versions(历史版本)、attachments(R2 附件记录)。写到 db/schema.sql,并告诉我怎么用 npx wrangler d1 execute --local 建到本地数据库、用 cf d1 migrations apply --dir db 建到线上数据库(cf 需要数据库的 uuid)。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
git add -A
git commit -m "Add D1 database"
git push

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

阶段 5:Firebase 登录

第 1 步:创建 Firebase 项目。

🎯 目标:创建 Firebase 项目。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  1. 打开 https://console.firebase.google.com ,使用 Google 账号登录。
  2. 点击「创建项目」(Create a project),输入项目名称 worship-songs(若名称已被占用,Firebase 会在末尾追加字符)。
  3. 询问是否启用 Google Analytics 时,关闭该选项,点击「创建项目」。
  4. 项目创建完成后,点击「继续」。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 5 第 1 步:创建 Firebase 项目」:在 Firebase 控制台创建名为 worship-songs 的新项目。
请带我按讲义步骤操作:打开 https://console.firebase.google.com 登录 Google 账号,创建名为 worship-songs 的项目,关闭 Google Analytics 后完成创建并进入项目。
完成标准(✅):在 Firebase 控制台成功创建名为 worship-songs 的项目并进入控制台概览页。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 2 步:开启登录方式。

🎯 目标:开启登录方式。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  1. 在左侧菜单点击「构建 → Authentication」(Build → Authentication),点击「开始使用」(Get started)。
  2. 切换到「登录方法」(Sign-in method) 标签页:
    • 点击「电子邮件地址/密码」(Email/Password),打开第一个「启用」开关,点击「保存」。
    • 点击「添加新的提供方」(Add new provider) → 选择「Google」,打开「启用」开关,「项目支持电子邮件」选择个人邮箱,点击「保存」。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 5 第 2 步:开启登录方式」:在 Firebase Authentication 中启用邮箱密码和 Google 登录方式。
请按讲义步骤指导我:在「构建 → Authentication」的「登录方法」标签页中,分别启用「电子邮件地址/密码」和「Google」提供方(支持邮箱选择个人邮箱并保存)。
完成标准(✅):Firebase Authentication 中「电子邮件地址/密码」和「Google」登录提供方均显示为已启用状态。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 3 步:创建 Realtime Database(后续用于实时评论,在登录代码中一并初始化):

🎯 目标:创建 Realtime Database。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  1. 在左侧菜单点击「构建 → Realtime Database」,点击「创建数据库」(Create Database)。
  2. 数据库位置选择 新加坡 Singapore (asia-southeast1),点击「下一步」。
  3. 选择「以锁定模式启动」(Start in locked mode),点击「启用」。
  4. 创建后,页面顶部显示数据库网址,格式如 https://【你的项目ID】-default-rtdb.asia-southeast1.firebasedatabase.app。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 5 第 3 步:创建 Realtime Database」:在 Firebase 中创建用于后续实时功能的 Realtime Database。
请按讲义步骤指导我:在「构建 → Realtime Database」中点击「创建数据库」,位置选择新加坡 Singapore (asia-southeast1),以锁定模式启动并启用,并记录顶部生成的数据库网址。
完成标准(✅):Realtime Database 成功创建(位置为 asia-southeast1),并获取到格式为 https://【你的项目ID】-default-rtdb.asia-southeast1.firebasedatabase.app 的数据库网址。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 4 步:注册网页应用,获取配置信息。

🎯 目标:注册网页应用,获取配置信息。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  1. 点击页面左上角的齿轮图标 ⚙️ → 选择「项目设置」(Project settings)。
  2. 向下滚动找到「您的应用」(Your apps) 区域,点击网页图标 </>。
  3. 应用昵称输入 worship-web,不要勾选 Firebase Hosting,点击「注册应用」。
  4. 页面将显示包含 const firebaseConfig = { apiKey: "...", authDomain: "...", ... } 的代码。保持该页面开启,后续步骤需要使用其中的配置。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 5 第 4 步:注册网页应用,获取配置信息」:在 Firebase 项目中注册 Web 应用并获取前端配置代码。
请带我操作:进入「项目设置」,在「您的应用」中点击网页图标 </>,昵称输入 worship-web(不勾选 Firebase Hosting)注册应用,并保留包含 firebaseConfig 的页面。
完成标准(✅):网页应用 worship-web 注册成功,并获取到包含 apiKey、authDomain、projectId 等配置信息的 firebaseConfig 代码。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 5 步:新建 .env.local 文件(位于项目根目录,与 package.json 同级),写入以下内容,并将各【…】替换为上一步中的对应值:

🎯 目标:新建 .env.local 文件。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

📌 以 NEXT_PUBLIC_ 开头的变量属于前端公开配置,非后台私密凭证。.env.local 已包含在 .gitignore 中,不会上传至 GitHub。 📌 这些配置会在构建打包时写入代码中。修改 .env.local 后,必须重新运行 npm run dev 或 npm run deploy 才能生效。 若第 4 步的 firebaseConfig 中未显示 databaseURL,填入第 3 步获取的数据库网址即可。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 5 第 5 步:新建 .env.local 文件」:在项目根目录创建环境变量文件并填入 Firebase 前端公开配置。
下面「参考资料」里有完整代码,请原样写入 .env.local(不要改动内容),写完告诉我怎么确认保存成功。
写完后请指导我将其中的各【…】占位符替换为 Firebase 控制台中的真实配置值(API_KEY、AUTH_DOMAIN、DATABASE_URL、PROJECT_ID、APP_ID)。
完成标准(✅):项目根目录成功创建 .env.local 文件,包含五个以 NEXT_PUBLIC_FIREBASE_ 开头的公开环境变量且已填入真实值。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
# 从 Firebase 控制台 → 项目设置 → 你的应用 → SDK 配置 里复制(这些是公开的前端配置,不是密码)
NEXT_PUBLIC_FIREBASE_API_KEY=【替换成你的 apiKey】
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=【你的项目ID】.firebaseapp.com
NEXT_PUBLIC_FIREBASE_DATABASE_URL=【替换成你的 databaseURL】
NEXT_PUBLIC_FIREBASE_PROJECT_ID=【你的项目ID】
NEXT_PUBLIC_FIREBASE_APP_ID=【替换成你的 appId】

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 6 步:安装 Firebase SDK 与用于验证令牌的 jose 库:

🎯 目标:安装 Firebase SDK 与用于验证令牌的 jose 库:。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 5 第 6 步:安装 Firebase SDK 与用于验证令牌的 jose 库」:安装客户端 Firebase SDK 与服务端验证 JWT 令牌所需的 jose 库。
请指导我运行安装命令:npm install firebase jose。
完成标准(✅):npm install firebase jose 执行成功,package.json 的 dependencies 中出现 firebase 和 jose。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm install firebase jose

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 7 步:新建或覆盖以下代码文件。

🎯 目标:新建或覆盖以下代码文件。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 5 第 7 步:新建或覆盖以下代码文件」:添加 Firebase 客户端连接、服务端令牌校验、API 路由及登录页面组件。
下面「参考资料」里有完整代码,请按路径原样写入对应路径(不要改动内容),写完告诉我怎么确认保存成功。
涉及文件包括新建:src/lib/firebase.ts、src/lib/server.ts、src/app/api/me/route.ts、src/components/AuthProvider.tsx、src/components/RequireLogin.tsx、src/components/Nav.tsx、src/app/login/page.tsx,以及覆盖:src/app/layout.tsx、src/app/globals.css。
完成标准(✅):上述 9 个代码文件全部新建或覆盖保存成功,项目中无语法或引用报错。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)

`src/lib/firebase.ts`(新建)——浏览器端连接 Firebase,并提供请求时附带登录令牌的 `authFetch` 函数:

ts
"use client";
import { initializeApp, getApps } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getDatabase } from "firebase/database";
    

const firebaseConfig = { apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL, projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, };

export const app = getApps().length ? getApps()[0] : initializeApp(firebaseConfig); export const auth = getAuth(app); export const rtdb = getDatabase(app);

// Call our own API with the Firebase ID token attached. export async function authFetch(url: string, init: RequestInit = {}) { const user = auth.currentUser; const headers = new Headers(init.headers); if (user) headers.set("Authorization", Bearer ${await user.getIdToken()}); return fetch(url, { ...init, headers }); }</code></pre></div>

<p class="ai-ref-label"><em>`src/lib/server.ts`(新建)——服务端:验证令牌、将新用户存入 D1(首位登录用户自动设为 leader)、判断权限:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">ts</span></div><pre><code class="language-ts">import { getCloudflareContext } from &quot;@opennextjs/cloudflare&quot;; import { createRemoteJWKSet, jwtVerify } from &quot;jose&quot;;

export type Role = "leader" | "editor" | "viewer"; export type AppUser = { id: string; email: string; display_name: string | null; role: Role };

// Google's public keys for verifying Firebase ID tokens. const JWKS = createRemoteJWKSet( new URL("https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com") );

export function getEnv() { return getCloudflareContext().env; }

export function json(data: unknown, status = 200) { return Response.json(data, { status }); }

// Returns the signed-in user (creating them in D1 on first visit), or null. export async function getUser(req: Request): Promise<AppUser | null> { const header = req.headers.get("authorization") ?? ""; if (!header.startsWith("Bearer ")) return null; const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!; try { const { payload } = await jwtVerify(header.slice(7), JWKS, { issuer: https://securetoken.google.com/${projectId}, audience: projectId, }); const uid = payload.sub!; const email = String(payload.email ?? ""); const name = payload.name ? String(payload.name) : null; const db = getEnv().DB; // The very first user becomes the worship leader (admin); everyone else starts as viewer. await db .prepare( INSERT OR IGNORE INTO users (id, email, display_name, role) VALUES (?1, ?2, ?3, CASE WHEN (SELECT COUNT(*) FROM users) = 0 THEN 'leader' ELSE 'viewer' END) ) .bind(uid, email, name) .run(); return await db.prepare("SELECT id, email, display_name, role FROM users WHERE id = ?1").bind(uid).first<AppUser>(); } catch { return null; } }

const rank: Record<Role, number> = { viewer: 1, editor: 2, leader: 3 }; export function can(user: AppUser | null, need: Role) { return !!user && rank[user.role] >= rank[need]; }</code></pre></div>

<p class="ai-ref-label"><em>`src/app/api/me/route.ts`(新建)——获取当前登录用户信息接口:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">ts</span></div><pre><code class="language-ts">import { getUser, json } from &quot;@/lib/server&quot;;

export async function GET(req: Request) { const user = await getUser(req); if (!user) return json({ error: "Not signed in" }, 401); return json(user); }</code></pre></div>

<p class="ai-ref-label"><em>`src/components/AuthProvider.tsx`(新建)——管理并提供当前登录状态:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">tsx</span></div><pre><code class="language-tsx">&quot;use client&quot;; import { createContext, useContext, useEffect, useState } from &quot;react&quot;; import { onAuthStateChanged, signOut, type User } from &quot;firebase/auth&quot;; import { auth, authFetch } from &quot;@/lib/firebase&quot;;

type Me = { id: string; email: string; display_name: string | null; role: "leader" | "editor" | "viewer" }; type AuthState = { user: User | null; me: Me | null; loading: boolean; logout: () => Promise<void> };

const AuthContext = createContext<AuthState>({ user: null, me: null, loading: true, logout: async () => {} });

export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null); const [me, setMe] = useState<Me | null>(null); const [loading, setLoading] = useState(true);

useEffect(() => { return onAuthStateChanged(auth, async (u) => { setUser(u); if (u) { const res = await authFetch("/api/me"); setMe(res.ok ? ((await res.json()) as Me) : null); } else { setMe(null); } setLoading(false); }); }, []);

return ( <AuthContext.Provider value={{ user, me, loading, logout: () => signOut(auth) }}>{children}</AuthContext.Provider> ); }

export const useAuth = () => useContext(AuthContext);</code></pre></div>

<p class="ai-ref-label"><em>`src/components/RequireLogin.tsx`(新建)——未登录时重定向至登录页:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">tsx</span></div><pre><code class="language-tsx">&quot;use client&quot;; import { useEffect } from &quot;react&quot;; import { useRouter } from &quot;next/navigation&quot;; import { useAuth } from &quot;./AuthProvider&quot;;

export function RequireLogin({ children }: { children: React.ReactNode }) { const { user, loading } = useAuth(); const router = useRouter(); useEffect(() => { if (!loading && !user) router.replace("/login"); }, [loading, user, router]); if (loading || !user) return <p className="p-6">加载中…</p>; return <>{children}</>; }</code></pre></div>

<p class="ai-ref-label"><em>`src/components/Nav.tsx`(新建)——顶部导航栏:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">tsx</span></div><pre><code class="language-tsx">&quot;use client&quot;; import Link from &quot;next/link&quot;; import { useAuth } from &quot;./AuthProvider&quot;;

export function Nav() { const { me, logout } = useAuth(); return ( <nav className="no-print flex items-center gap-4 border-b px-6 py-3"> <Link href="/" className="font-bold">🎵 敬拜歌谱</Link> {me && (me.role === "leader" || me.role === "editor") && <Link href="/songs/new">新建歌曲</Link>} {me?.role === "leader" && <Link href="/admin">成员管理</Link>} <span className="ml-auto text-sm text-gray-500"> {me ? ${me.display_name ?? me.email}(${me.role}) : ""} </span> {me ? <button onClick={logout} className="text-sm underline">退出</button> : <Link href="/login">登录</Link>} </nav> ); }</code></pre></div>

<p class="ai-ref-label"><em>&gt; 「成员管理」页面将在阶段 7 创建,当前点击显示 404 为正常现象。</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">tsx</span></div><pre><code class="language-tsx">import type { Metadata } from &quot;next&quot;; import &quot;./globals.css&quot;; import { AuthProvider } from &quot;@/components/AuthProvider&quot;; import { Nav } from &quot;@/components/Nav&quot;;

export const metadata: Metadata = { title: "敬拜歌谱", description: "Worship team song & lyrics editor", };

export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="zh-CN"> <body className="min-h-screen bg-white text-gray-900"> <AuthProvider> <Nav /> <main className="mx-auto max-w-5xl p-6">{children}</main> </AuthProvider> </body> </html> ); }</code></pre></div>

<p class="ai-ref-label"><em>`src/app/globals.css`(覆盖)——字体与打印样式(打印样式将在阶段 9 使用):</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">css</span></div><pre><code class="language-css">@import &quot;tailwindcss&quot;;

body { font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", Arial, sans-serif; }

/* ---------- 打印样式 Print layout ---------- */ @media print { @page { size: A4; margin: 15mm; } .no-print { display: none !important; } body { font-size: 14pt; color: #000; } .chord { color: #000 !important; } .section-title { break-after: avoid; } .lyric-line { break-inside: avoid; } .attachment-img { break-before: page; max-height: 260mm; } }</code></pre></div>

<p class="ai-ref-label"><em>`src/app/login/page.tsx`(新建)——登录 / 注册页面:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">tsx</span></div><pre><code class="language-tsx">&quot;use client&quot;; import { useState } from &quot;react&quot;; import { useRouter } from &quot;next/navigation&quot;; import { GoogleAuthProvider, createUserWithEmailAndPassword, signInWithEmailAndPassword, signInWithPopup, updateProfile, } from &quot;firebase/auth&quot;; import { auth } from &quot;@/lib/firebase&quot;;

export default function LoginPage() { const router = useRouter(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [name, setName] = useState(""); const [isSignup, setIsSignup] = useState(false); const [error, setError] = useState("");

async function submit(e: React.FormEvent) { e.preventDefault(); setError(""); try { if (isSignup) { const cred = await createUserWithEmailAndPassword(auth, email, password); if (name) await updateProfile(cred.user, { displayName: name }); } else { await signInWithEmailAndPassword(auth, email, password); } window.location.href = "/"; // full reload so the name shows everywhere } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }

async function google() { setError(""); try { await signInWithPopup(auth, new GoogleAuthProvider()); router.push("/"); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }

const input = "border rounded px-2 py-1 w-full"; return ( <div className="mx-auto max-w-sm space-y-4"> <h1 className="text-2xl font-bold">{isSignup ? "注册" : "登录"}</h1> <form onSubmit={submit} className="space-y-3"> {isSignup && <input className={input} placeholder="你的名字" value={name} onChange={(e) => setName(e.target.value)} />} <input className={input} type="email" placeholder="邮箱" value={email} onChange={(e) => setEmail(e.target.value)} required /> <input className={input} type="password" placeholder="密码(至少 6 位)" value={password} onChange={(e) => setPassword(e.target.value)} required /> <button className="w-full rounded bg-blue-600 py-2 text-white">{isSignup ? "注册" : "登录"}</button> </form> <button onClick={google} className="w-full rounded border py-2">用 Google 账号登录</button> <button onClick={() => setIsSignup(!isSignup)} className="text-sm underline"> {isSignup ? "已有账号?去登录" : "没有账号?去注册"} </button> {error && <p className="text-sm text-red-600">{error}</p>} </div> ); }</code></pre></div>

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 8 步:在本地测试登录功能。

🎯 目标:在本地测试登录功能。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  1. 在浏览器中打开 http://localhost:3000/login
  2. 点击「没有账号?去注册」,输入姓名、邮箱与密码(至少 6 位),点击「注册」。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 5 第 8 步:在本地测试登录功能」:在本地启动开发服务器测试注册流程并验证首个用户自动获得 leader 权限。
请指导我运行 npm run dev,并在浏览器打开 http://localhost:3000/login 点击「没有账号?去注册」完成注册测试。
完成标准(✅):页面跳转至首页,右上角显示 你的名字(leader)。本地数据库的首位注册用户会被自动设为 leader(主领)。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm run dev

✅ 检查点:页面跳转至首页,右上角显示 你的名字(leader)。本地数据库的首位注册用户将被自动设为 leader(主领)。

❌ 如果失败:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

第 9 步:存档。

🎯 目标:存档。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):


🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 5 第 9 步:存档」:将完成的 Firebase 登录功能代码提交并推送到 GitHub 远程仓库。
请指导我依次运行这些 Git 命令:git add -A、git commit -m "Add Firebase login"、git push。
完成标准(✅):登录功能相关代码成功 commit 并 push 到 GitHub 远程仓库。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)

🤖 路线 A 提示词:

text
我的 Next.js(App Router)项目部署在 Cloudflare Workers(@opennextjs/cloudflare),数据库是 D1(binding 名叫 DB),用户表结构在 db/schema.sql。
请帮我用 Firebase Authentication 做登录(邮箱密码 + Google):
1. 浏览器端用 firebase SDK 登录,配置从 NEXT_PUBLIC_FIREBASE_* 环境变量读取;调用自己的 /api 接口时在 Authorization 头里带上 ID token;
2. 后台不要用 firebase-admin(Workers 上不好用),改用 jose 库,按 Firebase 官方文档验证 ID token(JWKS、issuer、audience);
3. 验证通过后,如果用户不在 users 表里就插入:第一个用户的 role 是 leader,其他人是 viewer;
4. 做一个 /login 页面和一个 /api/me 接口。
请列出每个文件的完整代码。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
git add -A
git commit -m "Add Firebase login"
git push

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

阶段 6:歌曲列表、新建、编辑、历史版本

第 1 步:新建或覆盖以下文件。

🎯 目标:新建或覆盖以下文件。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 6 第 1 步:新建或覆盖以下文件」:添加歌曲列表、新建编辑、详情展示、ICC 歌词解析及历史版本功能代码。
下面「参考资料」里有完整代码,请按路径原样写入对应路径(不要改动内容),写完告诉我怎么确认保存成功。
涉及文件包括新建:src/app/api/songs/route.ts、src/app/api/songs/[id]/route.ts、src/lib/song-format.ts、src/components/SongRender.tsx、src/components/SongForm.tsx、src/app/songs/new/page.tsx、src/app/songs/[id]/page.tsx、src/app/songs/[id]/edit/page.tsx,以及覆盖:src/app/page.tsx。歌词解析遵循 ICC 格式(见讲义 2.2:英文分号 ; 分屏,@V1 段落名,# 演唱顺序,[G] 和弦)。
完成标准(✅):歌曲管理相关的 9 个文件全部新建或覆盖保存成功,项目中无缺失文件或语法错误。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)

`src/app/api/songs/route.ts`(新建)——歌曲列表(支持搜索)与新建歌曲的后台接口:

ts
import { can, getEnv, getUser, json } from "@/lib/server";
    

export async function GET(req: Request) { const user = await getUser(req); if (!user) return json({ error: "Not signed in" }, 401); const q = new URL(req.url).searchParams.get("q") ?? ""; const { results } = await getEnv() .DB.prepare( SELECT id, title, artist, song_key, tags, updated_at FROM songs WHERE title LIKE ?1 OR artist LIKE ?1 OR tags LIKE ?1 OR content LIKE ?1 ORDER BY updated_at DESC LIMIT 200 ) .bind(%${q}%) .all(); return json(results); }

export async function POST(req: Request) { const user = await getUser(req); if (!can(user, "editor")) return json({ error: "Editor role required" }, 403); const body = (await req.json()) as Record<string, string>; if (!body.title?.trim()) return json({ error: "Title is required" }, 400); const id = crypto.randomUUID(); await getEnv() .DB.prepare( INSERT INTO songs (id, title, artist, song_key, tempo, tags, content, created_by, updated_by) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?8) ) .bind(id, body.title.trim(), body.artist ?? null, body.song_key ?? null, Number(body.tempo) || null, body.tags ?? null, body.content ?? "", user!.id) .run(); return json({ id }, 201); }</code></pre></div>

<p class="ai-ref-label"><em>`src/app/api/songs/[id]/route.ts`(新建)——获取、更新(更新前自动保存历史版本)、删除歌曲。这是第二次课的版本,第三次课将补充删除附件功能:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">ts</span></div><pre><code class="language-ts">import { can, getEnv, getUser, json } from &quot;@/lib/server&quot;;

type Ctx = { params: Promise<{ id: string }> };

export async function GET(req: Request, { params }: Ctx) { const user = await getUser(req); if (!user) return json({ error: "Not signed in" }, 401); const { id } = await params; const song = await getEnv().DB.prepare("SELECT * FROM songs WHERE id = ?1").bind(id).first(); if (!song) return json({ error: "Not found" }, 404); return json(song); }

export async function PUT(req: Request, { params }: Ctx) { const user = await getUser(req); if (!can(user, "editor")) return json({ error: "Editor role required" }, 403); const { id } = await params; const body = (await req.json()) as Record<string, string>; if (!body.title?.trim()) return json({ error: "Title is required" }, 400); const db = getEnv().DB; // Save the old version first, then update (both in one batch). await db.batch([ db.prepare("INSERT INTO song_versions (song_id, title, content, edited_by) SELECT id, title, content, updated_by FROM songs WHERE id = ?1").bind(id), db .prepare( UPDATE songs SET title = ?2, artist = ?3, song_key = ?4, tempo = ?5, tags = ?6, content = ?7, updated_by = ?8, updated_at = datetime('now') WHERE id = ?1 ) .bind(id, body.title.trim(), body.artist ?? null, body.song_key ?? null, Number(body.tempo) || null, body.tags ?? null, body.content ?? "", user!.id), ]); return json({ ok: true }); }

export async function DELETE(req: Request, { params }: Ctx) { const user = await getUser(req); if (!can(user, "leader")) return json({ error: "Leader role required" }, 403); const { id } = await params; const env = getEnv(); // 第三次课(阶段 8)会在这里加上:先删除 R2 里的附件文件 await env.DB.batch([ env.DB.prepare("DELETE FROM attachments WHERE song_id = ?1").bind(id), env.DB.prepare("DELETE FROM song_versions WHERE song_id = ?1").bind(id), env.DB.prepare("DELETE FROM songs WHERE id = ?1").bind(id), ]); return json({ ok: true }); }</code></pre></div>

<p class="ai-ref-label"><em>`src/lib/song-format.ts`(新建)——解析歌词格式(详见 2.2),并提供导出到 ICC 歌词系统的 `toIccLyric()`:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">ts</span></div><pre><code class="language-ts">// Song content format, compatible with ICC Media Hub (icc-lyric-api). // &quot;;&quot; ends one screen (one click on the ICC control desk) // &quot;@V1&quot; group starting with @ = section name (V1 / V2 / C / B ...) // &quot;# V1 C&quot; group starting with # = play-order note (not shown on screen) // &quot;==Title&quot; line = big title, &quot;=Sub&quot; line = subtitle // &quot;[G]奇&quot; chord G above 奇 (our addition; removed by toIccContent) export type Line = | { kind: &quot;title&quot;; text: string } | { kind: &quot;subtitle&quot;; text: string } | { kind: &quot;lyric&quot;; pieces: { chord: string; text: string }[] }; export type Block = | { kind: &quot;section&quot;; label: string } | { kind: &quot;note&quot;; text: string } | { kind: &quot;screen&quot;; lines: Line[] };

const CHORD = /[([^]\n]*)]/g;

export function parseChords(line: string) { const pieces: { chord: string; text: string }[] = []; const re = /[([^]\n]*)]/g; let last = 0; let chord = ""; let m: RegExpExecArray | null; while ((m = re.exec(line))) { if (m.index > last || chord) pieces.push({ chord, text: line.slice(last, m.index) }); chord = m[1]; last = re.lastIndex; } pieces.push({ chord, text: line.slice(last) }); return pieces; }

function parseLine(line: string): Line { if (line.startsWith("==")) return { kind: "title", text: line.slice(2).trim() }; if (line.startsWith("=")) return { kind: "subtitle", text: line.slice(1).trim() }; return { kind: "lyric", pieces: parseChords(line) }; }

export function parseSong(content: string): Block[] { const groups = content.split(";").map((g) => g.trim()); if (groups.length > 0 && groups[groups.length - 1] === "") groups.pop(); // trailing ";" return groups.map((g): Block => { if (g.startsWith("@")) return { kind: "section", label: g.slice(1).trim() }; if (g.startsWith("#") || g.startsWith("$")) return { kind: "note", text: g.slice(1).trim() }; const lines = g.split("\n").map((l) => l.trim()).filter(Boolean); return { kind: "screen", lines: lines.map(parseLine) }; }); }

// Same text with chords removed: ready for ICC content. // Lines that only held chords (e.g. an intro "[G] [C]") are dropped. export function toIccContent(content: string): string { const strip = (group: string) => group .split("\n") .flatMap((line) => { const text = line.replace(CHORD, ""); return text.trim() === "" && line.trim() !== "" ? [] : [text]; }) .join("\n"); return content.split(";").map(strip).join(";"); }

// Payload for ICC POST /api/lyrics. ICC titles start with the pinyin initial // of the first character, e.g. "Q_奇异恩典"; pass it in when known. export function toIccLyric(song: { title: string; artist?: string | null; content: string }, prefix = "") { let content = toIccContent(song.content).trim(); if (!content.includes("==")) { const head = ["@Title;", ==《${song.title}》]; if (song.artist) head.push(=词曲:${song.artist}); content = ${head.join(&quot;\n&quot;)};\n${content}; } if (!content.endsWith(";")) content += ";"; return { title: prefix ? ${prefix.toUpperCase()}_${song.title} : song.title, content, language: /[\u4e00-\u9fff]/.test(song.title + content) ? "zh" : "en", } as const; }</code></pre></div>

<p class="ai-ref-label"><em>`src/components/SongRender.tsx`(新建)——将段落、分屏与 `[和弦]` 渲染为歌谱:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">tsx</span></div><pre><code class="language-tsx">// Renders song content (format: see src/lib/song-format.ts). import { parseSong } from &quot;@/lib/song-format&quot;;

export function SongRender({ content, showChords = true }: { content: string; showChords?: boolean }) { return ( <div className="song-body font-mono leading-relaxed"> {parseSong(content).map((b, i) => { if (b.kind === "section") return <h3 key={i} className="section-title mt-4 font-bold">{b.label}</h3>; if (b.kind === "note") return <p key={i} className="text-sm text-gray-500">顺序:{b.text}</p>; return ( <div key={i} className="screen mb-3"> {b.lines.map((l, j) => { if (l.kind === "title") return <h2 key={j} className="text-xl font-bold">{l.text}</h2>; if (l.kind === "subtitle") return <p key={j} className="text-gray-600">{l.text}</p>; return ( <div key={j} className="lyric-line flex flex-wrap items-end"> {l.pieces.map((p, k) => ( <span key={k} className="inline-flex flex-col whitespace-pre"> {showChords && <span className="chord text-sm font-bold text-blue-700">{p.chord || "\u00a0"}</span>} <span>{p.text || "\u00a0"}</span> </span> ))} </div> ); })} </div> ); })} </div> ); }</code></pre></div>

<p class="ai-ref-label"><em>`src/components/SongForm.tsx`(新建)——歌曲编辑表单,左侧编辑、右侧实时预览:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">tsx</span></div><pre><code class="language-tsx">&quot;use client&quot;; import { useState } from &quot;react&quot;; import { SongRender } from &quot;./SongRender&quot;;

export type SongInput = { title: string; artist: string; song_key: string; tempo: string; tags: string; content: string }; export const emptySong: SongInput = { title: "", artist: "", song_key: "", tempo: "", tags: "", content: "" };

export function SongForm({ initial, onSave }: { initial: SongInput; onSave: (s: SongInput) => Promise<void> }) { const [s, setS] = useState<SongInput>(initial); const [saving, setSaving] = useState(false); const set = (k: keyof SongInput) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setS({ ...s, [k]: e.target.value }); const input = "border rounded px-2 py-1 w-full";

return ( <form className="grid gap-4 md:grid-cols-2" onSubmit={async (e) => { e.preventDefault(); setSaving(true); try { await onSave(s); } finally { setSaving(false); } }} > <div className="space-y-3"> <input className={input} placeholder="歌名(必填)" value={s.title} onChange={set("title")} required /> <input className={input} placeholder="作者 / 原唱" value={s.artist} onChange={set("artist")} /> <div className="flex gap-2"> <input className={input} placeholder="调号,例如 G" value={s.song_key} onChange={set("song_key")} /> <input className={input} placeholder="速度 BPM" value={s.tempo} onChange={set("tempo")} /> </div> <input className={input} placeholder="标签,用逗号分隔" value={s.tags} onChange={set("tags")} /> <textarea className={${input} h-96 font-mono} placeholder={"@V1;\n[G]奇异恩[G7]典 何等[C]甘[G]甜\n我罪已得[Em]赦[D]免;"} value={s.content} onChange={set("content")} /> <p className="text-sm text-gray-500">格式:英文分号 ; 结束一屏;@V1 这样的一组是段落名;和弦写在方括号里,放在对应的字前面。</p> <button disabled={saving} className="rounded bg-blue-600 px-4 py-2 text-white">{saving ? "保存中…" : "保存"}</button> </div> <div className="rounded border p-4"> <p className="mb-2 text-sm text-gray-500">实时预览</p> <h2 className="text-xl font-bold">{s.title}</h2> <SongRender content={s.content} /> </div> </form> ); }</code></pre></div>

<p class="ai-ref-label"><em>`src/app/page.tsx`(覆盖)——首页:歌曲列表与搜索:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">tsx</span></div><pre><code class="language-tsx">&quot;use client&quot;; import Link from &quot;next/link&quot;; import { useEffect, useState } from &quot;react&quot;; import { authFetch } from &quot;@/lib/firebase&quot;; import { RequireLogin } from &quot;@/components/RequireLogin&quot;;

type Row = { id: string; title: string; artist: string | null; song_key: string | null; tags: string | null; updated_at: string };

function SongList() { const [q, setQ] = useState(""); const [songs, setSongs] = useState<Row[]>([]); useEffect(() => { const t = setTimeout(async () => { const res = await authFetch(/api/songs?q=${encodeURIComponent(q)}); if (res.ok) setSongs((await res.json()) as Row[]); }, 300); return () => clearTimeout(t); }, [q]);

return ( <div> <h1 className="mb-4 text-2xl font-bold">歌曲库</h1> <input className="mb-4 w-full rounded border px-3 py-2" placeholder="搜索歌名、作者、标签或歌词…" value={q} onChange={(e) => setQ(e.target.value)} /> <ul className="divide-y"> {songs.map((s) => ( <li key={s.id} className="py-3"> <Link href={/songs/${s.id}} className="font-semibold text-blue-700">{s.title}</Link> <span className="ml-2 text-sm text-gray-500">{[s.artist, s.song_key && 调: ${s.song_key}, s.tags].filter(Boolean).join(" · ")}</span> </li> ))} {songs.length === 0 && <li className="py-3 text-gray-500">还没有歌曲。</li>} </ul> </div> ); }

export default function Home() { return <RequireLogin><SongList /></RequireLogin>; }</code></pre></div>

<p class="ai-ref-label"><em>`src/app/songs/new/page.tsx`(新建)——新建歌曲页面:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">tsx</span></div><pre><code class="language-tsx">&quot;use client&quot;; import { useRouter } from &quot;next/navigation&quot;; import { authFetch } from &quot;@/lib/firebase&quot;; import { RequireLogin } from &quot;@/components/RequireLogin&quot;; import { SongForm, emptySong } from &quot;@/components/SongForm&quot;;

export default function NewSong() { const router = useRouter(); return ( <RequireLogin> <h1 className="mb-4 text-2xl font-bold">新建歌曲</h1> <SongForm initial={emptySong} onSave={async (s) => { const res = await authFetch("/api/songs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(s), }); const data = (await res.json()) as { id: string; error?: string }; if (!res.ok) return alert(data.error); router.push(/songs/${data.id}); }} /> </RequireLogin> ); }</code></pre></div>

<p class="ai-ref-label"><em>`src/app/songs/[id]/page.tsx`(新建)——歌曲详情页。这是第二次课的简化版,第三次课将补充附件与评论功能:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">tsx</span></div><pre><code class="language-tsx">&quot;use client&quot;; import Link from &quot;next/link&quot;; import { useParams, useRouter } from &quot;next/navigation&quot;; import { useEffect, useState } from &quot;react&quot;; import { authFetch } from &quot;@/lib/firebase&quot;; import { useAuth } from &quot;@/components/AuthProvider&quot;; import { RequireLogin } from &quot;@/components/RequireLogin&quot;; import { SongRender } from &quot;@/components/SongRender&quot;;

type Song = { id: string; title: string; artist: string | null; song_key: string | null; tempo: number | null; tags: string | null; content: string; updated_at: string };

function SongView({ id }: { id: string }) { const { me } = useAuth(); const router = useRouter(); const [song, setSong] = useState<Song | null>(null); const [showChords, setShowChords] = useState(true); const canEdit = me?.role === "leader" || me?.role === "editor";

useEffect(() => { authFetch(/api/songs/${id}).then(async (r) => r.ok && setSong((await r.json()) as Song)); }, [id]); if (!song) return <p>加载中…</p>;

async function del() { if (!confirm("确定删除这首歌?")) return; const res = await authFetch(/api/songs/${id}, { method: "DELETE" }); if (res.ok) router.push("/"); else alert(((await res.json()) as { error: string }).error); }

return ( <article> <div className="no-print mb-4 flex flex-wrap gap-3 text-sm"> <button onClick={() => window.print()} className="rounded bg-gray-800 px-3 py-1 text-white">🖨 打印</button> <label><input type="checkbox" checked={showChords} onChange={(e) => setShowChords(e.target.checked)} /> 显示和弦</label> {canEdit && <Link href={/songs/${id}/edit} className="rounded border px-3 py-1">✏️ 编辑</Link>} {me?.role === "leader" && <button onClick={del} className="rounded border border-red-500 px-3 py-1 text-red-600">删除</button>} </div> <h1 className="text-3xl font-bold">{song.title}</h1> <p className="mb-4 text-gray-600">{[song.artist, song.song_key && 调: ${song.song_key}, song.tempo && ${song.tempo} BPM].filter(Boolean).join(" · ")}</p> <SongRender content={song.content} showChords={showChords} /> {/* 第三次课会在这里加上:附件 Attachments + 评论 Comments */} </article> ); }

export default function SongPage() { const { id } = useParams<{ id: string }>(); return <RequireLogin><SongView id={id} /></RequireLogin>; }</code></pre></div>

<p class="ai-ref-label"><em>`src/app/songs/[id]/edit/page.tsx`(新建)——编辑歌曲页面:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">tsx</span></div><pre><code class="language-tsx">&quot;use client&quot;; import { useParams, useRouter } from &quot;next/navigation&quot;; import { useEffect, useState } from &quot;react&quot;; import { authFetch } from &quot;@/lib/firebase&quot;; import { RequireLogin } from &quot;@/components/RequireLogin&quot;; import { SongForm, type SongInput } from &quot;@/components/SongForm&quot;;

function EditSong({ id }: { id: string }) { const router = useRouter(); const [initial, setInitial] = useState<SongInput | null>(null); useEffect(() => { authFetch(/api/songs/${id}).then(async (r) => { if (!r.ok) return; const s = (await r.json()) as Record<string, string | number | null>; setInitial({ title: String(s.title ?? ""), artist: String(s.artist ?? ""), song_key: String(s.song_key ?? ""), tempo: s.tempo ? String(s.tempo) : "", tags: String(s.tags ?? ""), content: String(s.content ?? ""), }); }); }, [id]); if (!initial) return <p>加载中…</p>; return ( <> <h1 className="mb-4 text-2xl font-bold">编辑歌曲</h1> <SongForm initial={initial} onSave={async (s) => { const res = await authFetch(/api/songs/${id}, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(s), }); if (!res.ok) return alert(((await res.json()) as { error: string }).error); router.push(/songs/${id}); }} /> </> ); }

export default function EditPage() { const { id } = useParams<{ id: string }>(); return <RequireLogin><EditSong id={id} /></RequireLogin>; }</code></pre></div>

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 2 步:在本地测试歌曲功能。

🎯 目标:在本地测试歌曲功能。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

右侧预览区同步显示排版后的歌谱。点击「保存」后自动跳转至该歌曲详情页。 4. 点击「✏️ 编辑」,修改任意内容后保存。在新的终端窗口查询历史版本表:

能看到一条自动备份的历史版本记录。 5. 返回首页,在搜索框输入「哈利」,搜索结果显示刚才新建的歌曲。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 6 第 2 步:在本地测试歌曲功能」:验证歌曲列表、ICC 格式歌词与和弦显示、新建与编辑、历史版本和搜索。
测试顺序:npm run dev → 打开 http://localhost:3000 看示例歌曲 → 新建一首歌,歌词用 ICC 格式(如 @C; 段落 + [D]哈利[G]路亚 [A]赞美[D]主;)→ 编辑并保存 → 在新终端运行 npx wrangler d1 execute worship-db --local --command="SELECT song_id, title, created_at FROM song_versions" → 在首页搜索「哈利」。
完成标准(✅):首页显示示例歌曲且和弦排版正常;新建歌曲时右侧实时预览并能保存;编辑保存后 song_versions 查得到历史版本;搜索「哈利」能找到新建的歌曲。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm run dev
text
@C;
[D]哈利[G]路亚 [A]赞美[D]主;
bash
npx wrangler d1 execute worship-db --local --command="SELECT song_id, title, created_at FROM song_versions"
bash
npx wrangler d1 execute worship-db --local --command="UPDATE users SET role='leader' WHERE email='【你注册用的邮箱】'"

✅ 检查点(在浏览器打开 http://localhost:3000 ):

  1. 首页显示导入的示例歌曲「奇异恩典 Amazing Grace」。
  2. 点击进入,顶部显示「顺序:V1 V2」,下方显示段落名「V1」「V2」,和弦(蓝色小字)显示在对应歌词正上方;取消勾选「显示和弦」,和弦隐藏。
  3. 点击顶部导航栏的「新建歌曲」,填写歌名,在歌词输入框输入:

❌ 如果失败:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

第 3 步:部署到线上。

🎯 目标:部署到线上。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 6 第 3 步:部署到线上」:将包含歌曲管理与 ICC 排版功能的新版本部署到 Cloudflare Workers。
请指导我运行部署命令:npm run deploy。
完成标准(✅):npm run deploy 成功,终端显示线上网址 https://worship-songs.【你的子域名】.workers.dev,打开可看到歌曲列表。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm run deploy

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 4 步:将线上网址加入 Firebase 授权网域。

🎯 目标:将线上网址加入 Firebase 授权网域。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  1. 打开 Firebase 控制台 →「Authentication」→ 切换到「设置」(Settings) 标签页 → 找到「已获授权的网域」(Authorized domains)。
  2. 点击「添加网域」(Add domain),输入 worship-songs.【你的子域名】.workers.dev(注意:不要添加开头的 https://),点击「添加」。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 6 第 4 步:将线上网址加入 Firebase 授权网域」:在 Firebase 控制台添加线上 Workers 域名并测试线上注册与登录。
请带我操作:在 Firebase 控制台「Authentication → 设置 → 已获授权的网域」点击「添加网域」,输入 worship-songs.【你的子域名】.workers.dev(不带 https://);然后用手机打开【你的网址】/login 测试注册首位 leader 账号并测试 Google 登录。
完成标准(✅):手机打开【你的网址】/login 成功注册账号,右上角显示(leader)且可正常新建与编辑歌曲;Google 账号登录功能正常。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

✅ 检查点:

  1. 使用手机打开【你的网址】/login,注册账号。

    线上数据库为初始状态,在线上首个注册的用户会自动成为 leader,请先使用自己的账号完成首次注册。

  2. 登录后右上角显示 (leader),可正常新建与编辑歌曲。
  3. 测试「用 Google 账号登录」功能(可使用电脑上的其他浏览器或无痕窗口)。

❌ 如果失败:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

第 5 步:存档。

🎯 目标:存档。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 6 第 5 步:存档」:将阶段 6 完成的歌曲管理及历史版本功能代码推送到 GitHub 远程仓库。
请指导我依次运行这些 Git 命令:git add -A、git commit -m "Song list, editor and versions"、git push。
完成标准(✅):本地代码变更成功 commit 并 push 到 GitHub 远程仓库。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)

🤖 路线 A 提示词:

text
在我的 Next.js + Cloudflare D1 项目里(数据库结构见 db/schema.sql,后台工具函数在 src/lib/server.ts:getEnv、getUser、can、json;
前端用 src/lib/firebase.ts 里的 authFetch 调接口),请帮我做歌曲功能:
1. /api/songs:GET 列表(支持 ?q= 搜索歌名、作者、标签、歌词),POST 新建(需要 editor 以上);
2. /api/songs/[id]:GET、PUT(保存前先把旧版本复制到 song_versions,用 D1 batch)、DELETE(只有 leader);
3. 歌词格式与 ICC 歌词系统一致:英文分号 ; 结束一屏,以 @ 开头的一组是段落名(如 @V1),以 # 开头的一组是演唱顺序,[G] 这样的方括号是和弦,显示在后面那个字的上方;解析逻辑写在 src/lib/song-format.ts;
4. 页面:首页列表+搜索、/songs/new、/songs/[id](可勾选是否显示和弦)、/songs/[id]/edit(左边编辑右边实时预览)。
请给出每个文件的完整代码,并告诉我怎么测试。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
git add -A
git commit -m "Song list, editor and versions"
git push

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

8.4 ✅ 第二次课结束时你应该有

  • 可正常访问的线上网址,支持注册与登录,右上角显示姓名与 (leader)。
  • 可新建、编辑和搜索歌曲,和弦对齐显示在歌词上方。
  • GitHub 仓库已有 4–5 个代码存档。

8.5 作业(第三次课前完成)

  1. 若课上有未完成阶段,请参考讲义补齐(遇到报错可使用 1.3 的提示词向 AI 求助)。第三次课开场半小时为补进度时间,请尽量在课前完成至 M6。

9. 第三次课(11 月 21 日):辅导日——自己动手:角色、上传歌谱、打印、实时评论、上线展示

9.1 本次目标

在第二次课已上线的网站基础上,添加以下功能:

  1. 角色管理:leader(主领)可在网页上将成员设为 editor 或 viewer。
  2. 上传歌谱:支持上传图片或 PDF 歌谱,存储至 Cloudflare R2。
  3. 打印:生成适配 A4 纸的打印排版,自动隐藏网页按钮,可选择是否打印和弦。
  4. 实时评论 + 谁在线:借助 Firebase Realtime Database,支持查看歌谱时实时讨论与显示在线状态。
  5. 收尾上线,使用手机展示线上网址。

9.2 今天的里程碑(⏱ 参考时间,可按现场调整)

参考时间 里程碑 做哪些步骤 ✅ 怎么算完成(自己核对)
14:00 – 14:30 补齐 M6 未完成第二次课内容先补齐;已完成者直接开始阶段 7 对照 8.4
14:30 – 14:55 M7 角色与成员管理 阶段 7 阶段 7 的 ✅ 检查点:在「成员管理」将第二个测试账号改为 editor 后,显示「新建歌曲」
14:55 – 15:40 M8 上传歌谱到 R2 阶段 8(未绑定付款方式可按阶段 8 末尾「跳过 R2 的做法」直接进行阶段 9) 阶段 8 的 ✅ 检查点(跳过阶段 8 则无需核对)
15:40 – 15:55 休息
15:55 – 16:15 M9 打印 阶段 9 阶段 9 的 ✅ 检查点:打印预览中无导航栏和按钮,排版正常
16:15 – 17:00 M10 实时评论 + 谁在线 阶段 10 阶段 10 的 ✅ 检查点:手机和电脑打开线上网址中的同一首歌,评论实时同步
17:00 – 17:25 M11 收尾上线 阶段 11 第 1 步:修复遗留问题,按流程存档、测试、部署 线上网址为最新版本,代码已存档
17:25 – 17:55 成果展示 阶段 11 第 2 步:前 15 分钟将网址发送至群内,互相使用手机访问测试;后 15 分钟邀请 3–4 位学员上台各分享 2 分钟 线上网址可正常访问
17:55 – 18:00 总结、同心祷告、合影

遇到问题的排查顺序与第二次课相同:① 查看「❌ 如果失败」;② 使用 1.3 的 🆘 提示词询问 AI;③ 超过 10 分钟未解决,贴红色便利贴求助。未完成的阶段可参考讲义后续独立完成。


阶段 7:角色与成员管理

第 1 步:新建或覆盖以下文件。

🎯 目标:新建或覆盖以下文件。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 7 第 1 步:新建或覆盖以下文件」:添加成员管理页面与修改角色权限的后端接口。
下面「参考资料」里有完整代码,请原样写入对应文件(不要改动内容),写完告诉我怎么确认保存成功。涉及文件包括:新建 src/app/api/users/route.ts 和 src/app/admin/page.tsx。
完成标准(✅):src/app/api/users/route.ts 与 src/app/admin/page.tsx 两个文件成功新建并保存。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)

`src/app/api/users/route.ts`(新建)——获取成员列表与修改角色的后端接口(仅 leader 可调用):

ts
import { can, getEnv, getUser, json } from "@/lib/server";
    

export async function GET(req: Request) { const user = await getUser(req); if (!can(user, "leader")) return json({ error: "Leader role required" }, 403); const { results } = await getEnv().DB.prepare("SELECT id, email, display_name, role FROM users ORDER BY created_at").all(); return json(results); }

export async function PATCH(req: Request) { const user = await getUser(req); if (!can(user, "leader")) return json({ error: "Leader role required" }, 403); const { id, role } = (await req.json()) as { id: string; role: string }; if (!["leader", "editor", "viewer"].includes(role)) return json({ error: "Bad role" }, 400); await getEnv().DB.prepare("UPDATE users SET role = ?2 WHERE id = ?1").bind(id, role).run(); return json({ ok: true }); }</code></pre></div>

<p class="ai-ref-label"><em>`src/app/admin/page.tsx`(新建)——成员管理页面:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">tsx</span></div><pre><code class="language-tsx">&quot;use client&quot;; import { useEffect, useState } from &quot;react&quot;; import { authFetch } from &quot;@/lib/firebase&quot;; import { useAuth } from &quot;@/components/AuthProvider&quot;; import { RequireLogin } from &quot;@/components/RequireLogin&quot;;

type U = { id: string; email: string; display_name: string | null; role: string };

function Admin() { const { me } = useAuth(); const [users, setUsers] = useState<U[]>([]); const load = () => authFetch("/api/users").then(async (r) => r.ok && setUsers((await r.json()) as U[])); useEffect(() => { load(); }, []); if (me?.role !== "leader") return <p>只有敬拜主领(leader)可以管理成员。</p>;

async function change(id: string, role: string) { await authFetch("/api/users", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, role }) }); load(); }

return ( <div> <h1 className="mb-4 text-2xl font-bold">成员管理</h1> <table className="w-full text-left"> <thead><tr><th>名字</th><th>邮箱</th><th>角色</th></tr></thead> <tbody> {users.map((u) => ( <tr key={u.id} className="border-t"> <td className="py-2">{u.display_name ?? "-"}</td> <td>{u.email}</td> <td> <select value={u.role} onChange={(e) => change(u.id, e.target.value)} className="rounded border px-2 py-1"> <option value="leader">leader 主领</option> <option value="editor">editor 编辑</option> <option value="viewer">viewer 只读</option> </select> </td> </tr> ))} </tbody> </table> </div> ); }

export default function AdminPage() { return <RequireLogin><Admin /></RequireLogin>; }</code></pre></div>

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 2 步:在本地测试角色管理。

🎯 目标:在本地测试角色管理。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  1. 使用 leader 账号登录,点击顶部导航栏的「成员管理」。
  2. 打开浏览器的无痕窗口(Chrome 按 Ctrl + Shift + N,Mac 按 Command + Shift + N),访问 http://localhost:3000/login ,注册第二个测试账号。

权限说明:权限由后台接口校验(通过 can(user, "editor") 判断),不仅在前端隐藏按钮。伪造请求会被后台拦截并返回 403。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 7 第 2 步:在本地测试角色管理」:在本地使用多账号测试 viewer 权限限制与 leader 权限调整功能。
请指导我运行 npm run dev:用 leader 账号登录并打开「成员管理」;在无痕窗口打开 http://localhost:3000/login 注册第二个测试账号验证权限限制;在 leader 页面将其角色改为 editor,切回第二个账号窗口刷新验证按钮变化。
完成标准(✅):第二个账号登录后显示(viewer)且无新建与编辑权限;在 leader 窗口将其改为 editor 后,刷新页面变为(editor)并显示新建与编辑按钮(无删除按钮)。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm run dev

✅ 检查点:

  • 第二个账号登录后,右上角显示 (viewer),顶部不显示「新建歌曲」,进入歌曲详情页也没有「✏️ 编辑」按钮。
  • 切回 leader 窗口,刷新「成员管理」页面,在下拉框中将第二个账号的角色修改为 editor 编辑。
  • 切回第二个账号的窗口刷新页面,右上角变为 (editor),显示「新建歌曲」和「✏️ 编辑」按钮,无「删除」按钮(仅 leader 可删除歌曲)。

❌ 如果失败:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

第 3 步:部署并存档。

🎯 目标:部署并存档。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):


🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 7 第 3 步:部署并存档」:将角色与成员管理功能部署到 Cloudflare Workers 并在 GitHub 存档。
请指导我依次运行讲义中的命令:npm run deploy、git add -A、git commit -m "Roles and member admin"、git push。
完成标准(✅):部署上线成功,且角色与成员管理代码成功 commit 并 push 到 GitHub 远程仓库。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)

🤖 路线 A 提示词:

text
我的项目里 users 表有 role 字段(leader/editor/viewer),src/lib/server.ts 里有 can(user, role) 函数。
请帮我加一个只有 leader 能访问的 /admin 成员管理页面:列出所有用户,用下拉框修改角色;
后台接口 /api/users 的 GET 和 PATCH 都要检查 leader 权限,PATCH 只接受这三个角色。给出完整代码。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm run deploy
git add -A
git commit -m "Roles and member admin"
git push

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

阶段 8:上传歌谱到 R2

第 1 步:在 Cloudflare 控制台开通 R2。

🎯 目标:在 Cloudflare 控制台开通 R2。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  1. 打开 https://dash.cloudflare.com ,在左侧菜单找到「R2 Object Storage」(R2 对象存储)。
  2. 按照页面提示开通 R2 服务。

    📌 R2 提供免费额度(每月存储 10 GB)。开通时 Cloudflare 可能要求绑定付款方式(信用卡或 PayPal)。用量在免费额度内不会产生费用。若未绑定付款方式,可直接跳过阶段 8,进行阶段 9 和阶段 10(参见本阶段末尾「跳过 R2 的做法」)。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 8 第 1 步:在 Cloudflare 控制台开通 R2」:在 Cloudflare 控制台开通 R2 对象存储服务。
请指导我打开 https://dash.cloudflare.com 在左侧菜单点击「R2 Object Storage」,按照提示开通 R2 服务(免费额度每月 10 GB)。
完成标准(✅):在 Cloudflare 控制台成功开通 R2 Object Storage 存储服务。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 2 步:创建存储桶(bucket)。

🎯 目标:创建存储桶(bucket)。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

本地开发(npm run dev)使用的模拟存储桶由 OpenNext 自动创建,无需额外操作。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 8 第 2 步:创建存储桶(bucket)」:使用 cf CLI 在 Cloudflare 创建用于存放歌谱附件的 R2 存储桶。
请指导我在终端运行创建命令:cf r2 buckets create --name worship-files,并运行 cf r2 buckets list 查看确认。
完成标准(✅):终端输出的 JSON 中包含 "name": "worship-files",且运行 cf r2 buckets list 可在列表中看到该存储桶。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
cf r2 buckets create --name worship-files

✅ 检查点:终端输出的 JSON 中包含 "name": "worship-files"。运行 cf r2 buckets list 可在列表中看到该存储桶。

❌ 如果终端提示需要先去控制台开通 R2:返回第 1 步完成开通。:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

第 3 步:在 wrangler.jsonc 中添加 R2 绑定。 打开 wrangler.jsonc,替换为以下配置,并将【替换成 cf d1 create 输出的 uuid】替换为实际的 database_id(可从原文件复制,或运行 cf d1 list --name worship-db 查看 uuid):

🎯 目标:在 wrangler.jsonc 中添加 R2 绑定。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 8 第 3 步:在 wrangler.jsonc 中添加 R2 绑定」:在 wrangler.jsonc 中添加 R2 存储桶绑定配置。
下面「参考资料」里有完整代码,请原样写入 wrangler.jsonc(不要改动内容),写完告诉我怎么确认保存成功。
写完后请指导我将其中的【替换成 cf d1 create 输出的 uuid】替换为真实的【你的database_id】(可用 cf d1 list --name worship-db 查看)。
完成标准(✅):wrangler.jsonc 成功保存并同时包含 D1 数据库与 R2 存储桶(worship-files)的配置绑定。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "main": ".open-next/worker.js",
  "name": "worship-songs",
  "compatibility_date": "2026-11-04",
  "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
  "assets": {
    "directory": ".open-next/assets",
    "binding": "ASSETS"
  },
  "services": [
    {
      "binding": "WORKER_SELF_REFERENCE",
      "service": "worship-songs"
    }
  ],
  "images": {
    "binding": "IMAGES"
  },
  // ↓↓↓ 我们自己加的两段 ↓↓↓
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "worship-db",
      "database_id": "【替换成 cf d1 create 输出的 uuid】"
    }
  ],
  "r2_buckets": [
    {
      "binding": "FILES",
      "bucket_name": "worship-files"
    }
  ]
}

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 4 步:重新生成类型文件。

🎯 目标:重新生成类型文件。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 8 第 4 步:重新生成类型文件」:重新生成包含 R2 存储桶绑定的 TypeScript 类型定义文件。
请指导我运行类型生成命令:npm run cf-typegen。
完成标准(✅):cloudflare-env.d.ts 中包含 FILES: R2Bucket。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm run cf-typegen

✅ 检查点:cloudflare-env.d.ts 中包含 FILES: R2Bucket。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 5 步:新建或覆盖以下文件。

🎯 目标:新建或覆盖以下文件。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 8 第 5 步:新建或覆盖以下文件」:添加歌谱附件上传、文件读取、附件展示组件以及删除联动后端接口。
下面「参考资料」里有完整代码,请按路径原样写入对应路径(不要改动内容),写完告诉我怎么确认保存成功。
涉及文件包括新建:src/app/api/songs/[id]/attachments/route.ts、src/app/api/files/[...key]/route.ts、src/components/Attachments.tsx,以及覆盖:src/app/api/songs/[id]/route.ts。
完成标准(✅):上述 4 个附件管理与服务文件全部新建或覆盖保存完成,且代码无缺失无语法错误。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)

`src/app/api/songs/[id]/attachments/route.ts`(新建)——列出附件与上传附件(支持 PNG、JPG、WEBP、PDF,最大 10 MB):

ts
import { can, getEnv, getUser, json } from "@/lib/server";
    

type Ctx = { params: Promise<{ id: string }> }; const MAX_BYTES = 10 * 1024 * 1024; // 10 MB const ALLOWED = ["image/png", "image/jpeg", "image/webp", "application/pdf"];

export async function GET(req: Request, { params }: Ctx) { const user = await getUser(req); if (!user) return json({ error: "Not signed in" }, 401); const { id } = await params; const { results } = await getEnv() .DB.prepare("SELECT * FROM attachments WHERE song_id = ?1 ORDER BY created_at DESC") .bind(id) .all(); return json(results); }

export async function POST(req: Request, { params }: Ctx) { const user = await getUser(req); if (!can(user, "editor")) return json({ error: "Editor role required" }, 403); const { id } = await params; const form = await req.formData(); const file = form.get("file"); if (!(file instanceof File)) return json({ error: "No file" }, 400); if (!ALLOWED.includes(file.type)) return json({ error: "Only PNG, JPG, WEBP or PDF" }, 400); if (file.size > MAX_BYTES) return json({ error: "File larger than 10 MB" }, 400); const env = getEnv(); const attId = crypto.randomUUID(); const key = songs/${id}/${attId}-${file.name.replace(/[^\w.\-]/g, &quot;_&quot;)}; await env.FILES.put(key, await file.arrayBuffer(), { httpMetadata: { contentType: file.type } }); await env.DB.prepare( "INSERT INTO attachments (id, song_id, r2_key, file_name, content_type, size, uploaded_by) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)" ) .bind(attId, id, key, file.name, file.type, file.size, user!.id) .run(); return json({ id: attId, r2_key: key }, 201); }</code></pre></div>

<p class="ai-ref-label"><em>`src/app/api/files/[...key]/route.ts`(新建,目录名为 `[...key]`,包含三个点)——从 R2 读取文件返回给浏览器:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">ts</span></div><pre><code class="language-ts">import { getEnv } from &quot;@/lib/server&quot;;

// Serves a file from R2. Keys contain a random UUID, so they are hard to guess. export async function GET(_req: Request, { params }: { params: Promise<{ key: string[] }> }) { const { key } = await params; const obj = await getEnv().FILES.get(key.map(decodeURIComponent).join("/")); if (!obj) return new Response("Not found", { status: 404 }); const headers = new Headers(); obj.writeHttpMetadata(headers); headers.set("etag", obj.httpEtag); headers.set("cache-control", "private, max-age=3600"); return new Response(obj.body, { headers }); }</code></pre></div>

<p class="ai-ref-label"><em>&gt; 📌 说明:读取文件的接口未校验登录状态(图片标签无法携带认证令牌),文件路径包含随机 UUID,具备防猜测性。若有版权或隐私需求,可让 AI 修改为需要登录访问。</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">tsx</span></div><pre><code class="language-tsx">&quot;use client&quot;; import { useEffect, useState } from &quot;react&quot;; import { authFetch } from &quot;@/lib/firebase&quot;;

type Att = { id: string; r2_key: string; file_name: string; content_type: string };

export function Attachments({ songId, canEdit }: { songId: string; canEdit: boolean }) { const [list, setList] = useState<Att[]>([]); const [msg, setMsg] = useState(""); const [version, setVersion] = useState(0); // bump to reload the list useEffect(() => { authFetch(/api/songs/${songId}/attachments).then(async (res) => { if (res.ok) setList((await res.json()) as Att[]); }); }, [songId, version]);

async function upload(e: React.ChangeEvent<HTMLInputElement>) { const file = e.target.files?.[0]; if (!file) return; setMsg("上传中…"); const fd = new FormData(); fd.append("file", file); const res = await authFetch(/api/songs/${songId}/attachments, { method: "POST", body: fd }); setMsg(res.ok ? "上传成功" : 上传失败:${((await res.json()) as { error: string }).error}); e.target.value = ""; setVersion((v) => v + 1); }

return ( <section className="mt-6"> <h3 className="font-bold no-print">附件(歌谱图片 / PDF)</h3> {canEdit && ( <div className="no-print my-2"> <input type="file" accept="image/png,image/jpeg,image/webp,application/pdf" onChange={upload} /> <span className="ml-2 text-sm">{msg}</span> </div> )} <div className="grid gap-3"> {list.map((a) => { const url = /api/files/${a.r2_key.split(&quot;/&quot;).map(encodeURIComponent).join(&quot;/&quot;)}; return a.content_type.startsWith("image/") ? ( // eslint-disable-next-line @next/next/no-img-element <img key={a.id} src={url} alt={a.file_name} className="attachment-img max-w-full border" /> ) : ( <a key={a.id} href={url} target="_blank" className="no-print text-blue-600 underline">📄 {a.file_name}</a> ); })} </div> </section> ); }</code></pre></div>

<p class="ai-ref-label"><em>`src/app/api/songs/[id]/route.ts`(覆盖)——删除歌曲时同时删除 R2 中的附件文件:</em></p> <div class="code-wrap ai-ref-code"><div class="code-head"><span class="lang">ts</span></div><pre><code class="language-ts">import { can, getEnv, getUser, json } from &quot;@/lib/server&quot;;

type Ctx = { params: Promise<{ id: string }> };

export async function GET(req: Request, { params }: Ctx) { const user = await getUser(req); if (!user) return json({ error: "Not signed in" }, 401); const { id } = await params; const song = await getEnv().DB.prepare("SELECT * FROM songs WHERE id = ?1").bind(id).first(); if (!song) return json({ error: "Not found" }, 404); return json(song); }

export async function PUT(req: Request, { params }: Ctx) { const user = await getUser(req); if (!can(user, "editor")) return json({ error: "Editor role required" }, 403); const { id } = await params; const body = (await req.json()) as Record<string, string>; if (!body.title?.trim()) return json({ error: "Title is required" }, 400); const db = getEnv().DB; // Save the old version first, then update (both in one batch). await db.batch([ db.prepare("INSERT INTO song_versions (song_id, title, content, edited_by) SELECT id, title, content, updated_by FROM songs WHERE id = ?1").bind(id), db .prepare( UPDATE songs SET title = ?2, artist = ?3, song_key = ?4, tempo = ?5, tags = ?6, content = ?7, updated_by = ?8, updated_at = datetime('now') WHERE id = ?1 ) .bind(id, body.title.trim(), body.artist ?? null, body.song_key ?? null, Number(body.tempo) || null, body.tags ?? null, body.content ?? "", user!.id), ]); return json({ ok: true }); }

export async function DELETE(req: Request, { params }: Ctx) { const user = await getUser(req); if (!can(user, "leader")) return json({ error: "Leader role required" }, 403); const { id } = await params; const env = getEnv(); const { results } = await env.DB.prepare("SELECT r2_key FROM attachments WHERE song_id = ?1").bind(id).all<{ r2_key: string }>(); for (const a of results) await env.FILES.delete(a.r2_key); await env.DB.batch([ env.DB.prepare("DELETE FROM attachments WHERE song_id = ?1").bind(id), env.DB.prepare("DELETE FROM song_versions WHERE song_id = ?1").bind(id), env.DB.prepare("DELETE FROM songs WHERE id = ?1").bind(id), ]); return json({ ok: true }); }</code></pre></div>

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 6 步:在歌曲详情页添加附件区。 打开 src/app/songs/[id]/page.tsx,进行以下两处修改:

🎯 目标:在歌曲详情页添加附件区。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  1. 找到该行:

在其下方添加:

  1. 找到该行:

在其下方添加:

保存文件。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 8 第 6 步:在歌曲详情页添加附件区」:在歌曲详情页中引入并挂载 Attachments 组件。
请指导我修改 src/app/songs/[id]/page.tsx:在 import { SongRender } from "@/components/SongRender"; 下方添加 import { Attachments } from "@/components/Attachments";;并在 {/* 第三次课会在这里加上:附件 Attachments + 评论 Comments */} 下方添加 <Attachments songId={id} canEdit={canEdit} />。
完成标准(✅):src/app/songs/[id]/page.tsx 成功引入并挂载 Attachments 组件,文件保存无语法错误。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
tsx
import { SongRender } from "@/components/SongRender";
tsx
import { Attachments } from "@/components/Attachments";
tsx
      {/* 第三次课会在这里加上:附件 Attachments + 评论 Comments */}
tsx
      <Attachments songId={id} canEdit={canEdit} />

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 7 步:在本地测试上传功能。

🎯 目标:在本地测试上传功能。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

本地开发时,R2 使用本地模拟存储,文件不会上传至 Cloudflare 云端。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 8 第 7 步:在本地测试上传功能」:在本地开发服务器测试歌谱图片与 PDF 附件上传及预览效果。
请指导我运行 npm run dev:在浏览器打开任意歌曲详情页,在「附件(歌谱图片 / PDF)」区域分别上传图片和 PDF 文件进行功能验证。
完成标准(✅):打开任意歌曲,下方显示「附件(歌谱图片 / PDF)」区域。选择歌谱图片上传,页面显示「上传成功」,图片展示在歌词下方;上传 PDF 文件,显示「📄 文件名」链接,点击可打开查看。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm run dev

✅ 检查点:打开任意歌曲,下方显示「附件(歌谱图片 / PDF)」区域。选择歌谱图片上传,页面显示「上传成功」,图片展示在歌词下方;上传 PDF 文件,显示「📄 文件名」链接,点击可打开查看。

❌ 如果失败:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

第 8 步:部署并存档。

🎯 目标:部署并存档。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

跳过 R2 的做法(未绑定付款方式): 可直接跳过第 1–8 步,进行阶段 9。在执行阶段 10 的第 4 步前,将以下提示词发送给 AI,移除附件相关代码:


🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 8 第 8 步:部署并存档」:将 R2 附件上传功能部署到线上环境并在 GitHub 远程仓库存档。
请指导我依次运行讲义中的命令:npm run deploy、git add -A、git commit -m "Upload song sheets to R2"、git push。
完成标准(✅):在线上网站打开任意歌曲并上传图片,进入 Cloudflare 控制台的「R2 → worship-files」,列表中显示以 songs/... 开头的文件即表示部署成功。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)

🤖 路线 A 提示词:

text
我的 Next.js 项目在 Cloudflare Workers 上运行,wrangler.jsonc 里已经绑定了 R2 存储桶(binding 名 FILES)和 D1(DB),
attachments 表结构在 db/schema.sql。请帮我做歌谱附件上传:
1. POST /api/songs/[id]/attachments:editor 以上可上传,只允许 png/jpeg/webp/pdf,最大 10 MB,存到 R2 的 songs/{歌曲id}/{uuid}-{文件名},并写入 attachments 表;
2. GET 同一个地址列出附件;
3. GET /api/files/[...key] 从 R2 读出文件返回给浏览器;
4. 一个 Attachments 组件:上传按钮 + 图片直接显示、PDF 显示为链接。用 getCloudflareContext().env 拿到 FILES。给出完整代码。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)

🤖 AI 提示词(跳过 R2 时使用):

text
我没有开通 Cloudflare R2。请帮我修改 src/app/songs/[id]/page.tsx,删掉 Attachments 组件的 import 和使用,保留 Comments。
同时确认 src/app/api/songs/[id]/route.ts 里没有用到 env.FILES。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm run deploy
git add -A
git commit -m "Upload song sheets to R2"
git push

✅ 检查点:在线上网站打开任意歌曲并上传图片,进入 Cloudflare 控制台的「R2 → worship-files」,列表中显示以 songs/... 开头的文件即表示部署成功。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

阶段 9:打印

阶段 5 写入 src/app/globals.css 的 @media print { ... } 已包含打印排版样式。具体规则说明如下:

规则 作用
@page { size: A4; margin: 15mm; } 按标准 A4 纸排版,四边各留出 15 毫米边距
.no-print { display: none } 打印时自动隐藏顶部导航栏、操作按钮、上传框和评论区
.chord { color: #000 } 打印时将和弦设为纯黑色,黑白打印清晰易读
.section-title { break-after: avoid } 避免段落标题留在上一页底部
.lyric-line { break-inside: avoid } 保证同一行歌词(上方和弦与下方文字)不被分页截断
.attachment-img { break-before: page } 使上传的歌谱图片另起一页打印

第 1 步:测试打印效果。

🎯 目标:测试打印效果。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  1. 打开一首歌,点击页面上的「🖨 打印」按钮。

  2. 在弹出的浏览器打印预览窗口查看效果(可直接选择「另存为 PDF」)。

  3. 关闭预览窗口,取消勾选页面上的「显示和弦」,再次点击「🖨 打印」。


🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 9 第 1 步:测试打印效果」:测试歌谱页面的浏览器打印与 PDF 导出排版效果。
请指导我打开一首歌点击「🖨 打印」查看浏览器打印预览(隐藏导航与按钮、附件分页);关闭预览后取消勾选「显示和弦」再次点击「🖨 打印」查看纯歌词打印效果。
完成标准(✅):打印预览中不显示导航栏和按钮;歌名、段落、和弦与歌词正常排版;若歌曲带有附件图片,图片从第二页开始显示;取消勾选「显示和弦」后打印预览仅显示歌词。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

✅ 检查点:打印预览中仅显示歌词,不显示和弦。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

阶段 10:实时评论 + 谁在线(Firebase Realtime Database)

第 1 步:设置数据库安全规则。

🎯 目标:设置数据库安全规则。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  1. 打开 Firebase 控制台 →「Realtime Database」→ 切换到「规则」(Rules) 标签页。

  2. 清空编辑框原有内容,粘贴以下安全规则(同时在项目根目录下新建 database.rules.json 文件,保存相同内容备份):

  3. 点击「发布」(Publish) 按钮。

该安全规则包含以下限制:

  • 仅已登录同工可查看评论内容与在线名单;
  • 用户仅可发表或删除自己的评论,单条评论上限 1000 字;
  • 用户仅可登记和修改自己的在线状态。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 10 第 1 步:设置数据库安全规则」:在 Firebase Realtime Database 中配置安全规则并在本地保存备份。
下面「参考资料」里有完整代码,请原样写入 database.rules.json(不要改动内容),写完告诉我怎么确认保存成功。
写完后请指导我在 Firebase 控制台「Realtime Database → 规则」中粘贴相同内容并点击「发布」。
完成标准(✅):Firebase 控制台 Realtime Database 安全规则成功发布,且根目录下成功生成 database.rules.json 备份文件。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
json
{
  "rules": {
    "comments": {
      "$songId": {
        ".read": "auth != null",
        "$commentId": {
          ".write": "auth != null && (!data.exists() || data.child('uid').val() === auth.uid)",
          ".validate": "!newData.exists() || (newData.child('uid').val() === auth.uid && newData.child('text').isString() && newData.child('text').val().length <= 1000)"
        }
      }
    },
    "presence": {
      "$songId": {
        ".read": "auth != null",
        "$uid": {
          ".write": "auth != null && auth.uid === $uid"
        }
      }
    }
  }
}

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 2 步:新建评论组件 src/components/Comments.tsx:

🎯 目标:新建评论组件 src/components/Comments.tsx:。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 10 第 2 步:新建评论组件 src/components/Comments.tsx」:创建实时评论与在线名单 presence 监听的 React 组件。
下面「参考资料」里有完整代码,请原样写入 src/components/Comments.tsx(不要改动内容),写完告诉我怎么确认保存成功。
完成标准(✅):src/components/Comments.tsx 文件成功创建并保存完整组件代码。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
tsx
"use client";
import { useEffect, useState } from "react";
import { onDisconnect, onValue, push, ref, remove, serverTimestamp, set } from "firebase/database";
import { rtdb } from "@/lib/firebase";
import { useAuth } from "./AuthProvider";
    

type Comment = { id: string; uid: string; name: string; text: string; createdAt: number };

export function Comments({ songId }: { songId: string }) { const { user } = useAuth(); const [comments, setComments] = useState<Comment[]>([]); const [online, setOnline] = useState<string[]>([]); const [text, setText] = useState("");

// Live comments: every open browser updates instantly. useEffect(() => { return onValue(ref(rtdb, comments/${songId}), (snap) => { const val = (snap.val() ?? {}) as Record<string, Omit<Comment, "id">>; setComments(Object.entries(val).map(([id, c]) => ({ id, ...c })).sort((a, b) => a.createdAt - b.createdAt)); }); }, [songId]);

// Presence: who is looking at this song right now. useEffect(() => { if (!user) return; const me = ref(rtdb, presence/${songId}/${user.uid}); set(me, user.displayName ?? user.email ?? "someone"); onDisconnect(me).remove(); const off = onValue(ref(rtdb, presence/${songId}), (snap) => setOnline(Object.values(snap.val() ?? {}))); return () => { off(); remove(me); }; }, [songId, user]);

async function send(e: React.FormEvent) { e.preventDefault(); if (!user || !text.trim()) return; await push(ref(rtdb, comments/${songId}), { uid: user.uid, name: user.displayName ?? user.email, text: text.trim(), createdAt: serverTimestamp(), }); setText(""); }

return ( <section className="no-print mt-8 rounded border p-4"> <h3 className="font-bold">讨论区</h3> <p className="text-sm text-green-700">🟢 正在看这首歌:{online.join("、") || "只有你"}</p> <ul className="my-3 space-y-2"> {comments.map((c) => ( <li key={c.id} className="text-sm"> <b>{c.name}</b>:{c.text} <span className="ml-2 text-gray-400">{c.createdAt ? new Date(c.createdAt).toLocaleString() : ""}</span> {c.uid === user?.uid && ( <button className="ml-2 text-red-500" onClick={() => remove(ref(rtdb, comments/${songId}/${c.id}))}>删除</button> )} </li> ))} </ul> <form onSubmit={send} className="flex gap-2"> <input className="flex-1 rounded border px-2 py-1" value={text} onChange={(e) => setText(e.target.value)} placeholder="写评论,例如:副歌第二遍降调" /> <button className="rounded bg-blue-600 px-3 text-white">发送</button> </form> </section> ); }</code></pre></div>

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 3 步:把歌曲详情页覆盖成最终版本。 打开 src/app/songs/[id]/page.tsx,将内容替换为:

🎯 目标:把歌曲详情页覆盖成最终版本。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

若此前跳过了 R2 步骤:请使用阶段 8 末尾提供的提示词,让 AI 删除与 Attachments 相关的两行代码。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 10 第 3 步:把歌曲详情页覆盖成最终版本」:将歌曲详情页更新为集成附件与实时评论组件的最终完整版本。
下面「参考资料」里有完整代码,请原样写入 src/app/songs/[id]/page.tsx(不要改动内容),写完告诉我怎么确认保存成功。
完成标准(✅):src/app/songs/[id]/page.tsx 成功替换为最终完整版本并保存。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
tsx
"use client";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { authFetch } from "@/lib/firebase";
import { useAuth } from "@/components/AuthProvider";
import { RequireLogin } from "@/components/RequireLogin";
import { SongRender } from "@/components/SongRender";
import { Attachments } from "@/components/Attachments";
import { Comments } from "@/components/Comments";
    

type Song = { id: string; title: string; artist: string | null; song_key: string | null; tempo: number | null; tags: string | null; content: string; updated_at: string };

function SongView({ id }: { id: string }) { const { me } = useAuth(); const router = useRouter(); const [song, setSong] = useState<Song | null>(null); const [showChords, setShowChords] = useState(true); const canEdit = me?.role === "leader" || me?.role === "editor";

useEffect(() => { authFetch(/api/songs/${id}).then(async (r) => r.ok && setSong((await r.json()) as Song)); }, [id]); if (!song) return <p>加载中…</p>;

async function del() { if (!confirm("确定删除这首歌?")) return; const res = await authFetch(/api/songs/${id}, { method: "DELETE" }); if (res.ok) router.push("/"); else alert(((await res.json()) as { error: string }).error); }

return ( <article> <div className="no-print mb-4 flex flex-wrap gap-3 text-sm"> <button onClick={() => window.print()} className="rounded bg-gray-800 px-3 py-1 text-white">🖨 打印</button> <label><input type="checkbox" checked={showChords} onChange={(e) => setShowChords(e.target.checked)} /> 显示和弦</label> {canEdit && <Link href={/songs/${id}/edit} className="rounded border px-3 py-1">✏️ 编辑</Link>} {me?.role === "leader" && <button onClick={del} className="rounded border border-red-500 px-3 py-1 text-red-600">删除</button>} </div> <h1 className="text-3xl font-bold">{song.title}</h1> <p className="mb-4 text-gray-600">{[song.artist, song.song_key && 调: ${song.song_key}, song.tempo && ${song.tempo} BPM].filter(Boolean).join(" · ")}</p> <SongRender content={song.content} showChords={showChords} /> <Attachments songId={id} canEdit={canEdit} /> <Comments songId={id} /> </article> ); }

export default function SongPage() { const { id } = useParams<{ id: string }>(); return <RequireLogin><SongView id={id} /></RequireLogin>; }</code></pre></div>

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 4 步:在本地测试——同时打开两个窗口验证实时效果。

🎯 目标:在本地测试——同时打开两个窗口验证实时效果。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  1. 在普通浏览器窗口登录账号 A,打开「奇异恩典」。
  2. 打开无痕窗口登录账号 B,打开同一首歌。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 10 第 4 步:在本地测试——同时打开两个窗口验证实时效果」:在本地开发环境中用两个浏览器窗口验证实时评论与在线名单同步效果。
请指导我运行 npm run dev:在普通窗口和无痕窗口分别登录账号 A 和账号 B 并打开同一首歌,验证在线名单、即时发送显示评论、关闭窗口离线更新及打印时隐藏讨论区。
完成标准(✅):两个窗口均显示双方在线;A 发送评论 B 无需刷新实时显示;关闭 B 窗口数秒后在线名单自动消失;仅本人评论显示删除;打印预览中讨论区自动隐藏。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm run dev

✅ 检查点:

  • 两个窗口均显示「🟢 正在看这首歌:A、B」。
  • 在 A 窗口输入评论「副歌第二遍降调」并点击「发送」,B 窗口无需刷新即可实时显示该评论。
  • 关闭 B 窗口,数秒后 A 窗口在线名单中的 B 自动消失。
  • 当前用户发表的评论显示「删除」按钮,其他用户的评论不显示。
  • 点击「🖨 打印」,讨论区在打印预览中自动隐藏。

❌ 如果失败:把终端或浏览器里的报错全文复制给 AI,用本步或 1.3 的 🆘 提示词。

第 5 步:部署并存档。

🎯 目标:部署并存档。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):


🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 10 第 5 步:部署并存档」:将实时评论与在线名单功能部署上线并在 GitHub 存档。
请指导我依次运行讲义中的命令:npm run deploy、git add -A、git commit -m "Realtime comments and presence"、git push。
完成标准(✅):在手机和电脑上分别打开线上网址的同一首歌并发送评论,两端均可实时同步显示。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)

🤖 路线 A 提示词:

text
我的 Next.js 项目已经用 Firebase Authentication 登录,src/lib/firebase.ts 导出了 rtdb(Realtime Database)和 auth。
请帮我做一个 Comments 组件,放在歌曲详情页:
1. 评论存在 comments/{songId}/{自动id},内容有 uid、name、text、createdAt(serverTimestamp),用 onValue 实时显示;
2. 在线状态存在 presence/{songId}/{uid},用 onDisconnect 自动删除,显示「正在看这首歌:…」;
3. 自己的评论可以删除;
4. 给我对应的 Realtime Database 安全规则:只有登录的人能读,只能写自己的评论和自己的在线状态,评论最长 1000 字。
5. 整个评论区打印时隐藏(加 no-print 类)。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
npm run deploy
git add -A
git commit -m "Realtime comments and presence"
git push

✅ 检查点:在手机和电脑上分别打开线上网址的同一首歌并发送评论,两端均可实时同步显示。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

阶段 11:收尾上线与展示

第 1 步:使用 AI 修改代码的安全流程:

🎯 目标:使用 AI 修改代码的安全流程:。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  1. 修改前存档:运行 git add -A → git commit -m "before 【功能名】"。
  2. 要求 AI 说明计划:在提示词末尾加上:「先告诉我你打算改哪些文件、怎么改,等我确认后再动手写代码」。
  3. 本地测试:运行 npm run dev,按照 AI 提供的测试步骤逐项验证。
  4. 测试通过后发布:运行 git add -A → git commit -m "【功能名】" → git push → npm run deploy 正式发布。
  5. 代码异常时回退:运行以下命令回退到修改前的存档(注意:未存档的修改将被丢弃):

其中 git clean -fd 会删除未存档的新建文件。如不确定,可先仅运行 git restore .,或咨询 AI 安全撤回方案。

  1. 查看历史提交记录:运行 git log --oneline。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 11 第 1 步:使用 AI 修改代码的安全流程」:我想给网站增加【功能名】,请按以下流程配合我:
1. 先让我运行 git add -A 和 git commit -m "before 【功能名】" 存档;2. 先告诉我你打算改哪些文件、怎么改,等我确认后再写代码;3. 写完后给我本地测试步骤(npm run dev);4. 测试通过后带我运行 git add -A、git commit -m "【功能名】"、git push、npm run deploy;5. 如果改坏了,带我用 git restore . 回退(运行 git clean -fd 前先提醒我它会删除未存档的新文件)。
完成标准(✅):新功能在本地测试通过并已上线,git log --oneline 能看到修改前后的两次存档。
请一步一步带我做:每次只给一条命令或一个操作,等我贴回结果再继续;出错时先解释原因再给修复方法。

(本提示末尾附有「参考资料」:代码与命令请原样使用,不要改写。)
给 AI 的参考资料(不用看,点「复制提示词」会自动带上)
bash
git restore .
git clean -fd

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

第 2 步:成果展示(17:25)。 28 位学员在 30 分钟内分两段进行展示:前 15 分钟,将项目网址发送到群内,使用手机互相访问测试;后 15 分钟,请 3–4 位学员上台各分享 2 分钟:

🎯 目标:成果展示(17:25)。交给 AI 来写代码、跑命令;你负责按提示操作界面并核对 ✅。

👉 你来做(界面操作):

  • 引导学员使用手机打开项目网址;
  • 展示敬拜团常用诗歌(包括和弦排版与打印预览效果);
  • 说明制作过程中遇到的问题及解决方案。

🤖 AI 提示词(出错时改用 1.3 的 🆘 万能提示词):

text
我在做课程项目「敬拜团歌谱编辑系统」(Next.js + Cloudflare D1/R2 + Firebase),电脑是【我的系统】。
现在做「阶段 11 第 2 步:成果展示」:我要用 2 分钟展示项目,线上网址是【你的网址】。
请帮我:1. 列一份上台前的检查清单(手机能打开网址、能登录、示例歌曲的和弦排版与打印预览正常);2. 写一份 2 分钟讲稿:网站功能演示(敬拜团常用诗歌、和弦排版、打印预览)+ 制作中遇到的一个问题及解决方法。
完成标准(✅):同学用手机能打开我的网址,我能按讲稿完成 2 分钟展示。
请一步一步带我做:每次只给一项检查或一个操作,等我反馈结果再继续;发现问题时先解释原因再给修复方法。

✅ 按提示词完成后,确认页面或终端出现预期结果。

❌ 如果失败:把报错全文复制给 AI,用 1.3 的 🆘 提示词。

9.3 ✅ 第三次课结束时你应该有

  • 功能完整的线上敬拜团歌谱系统:涵盖注册登录、角色权限、歌曲编辑、历史版本备份、上传歌谱附件、A4 打印排版与实时在线讨论。
  • GitHub 私有仓库,保存每一步的提交历史。
  • 掌握「先存档 → 让 AI 改 → 本地测试 → 部署上线」安全工作流,支持后续功能扩展。

9.4 作业(课后)

  1. 邀请敬拜团 2–3 位同工注册登录,在「成员管理」中设置为 editor,录入当月诗歌数据。
  2. 若遇到报错,可将 1.3 的「我卡住了」提示词连同报错信息发送给 AI;若无法解决,联系老师(WhatsApp 马伦 81311318)。

附录 A:分系统速查表

要装的东西 Windows 10/11(PowerShell) macOS(Apple 芯片 / Intel) Linux(Ubuntu) 验证命令
执行策略 Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser 不需要 不需要 Get-ExecutionPolicy -Scope CurrentUser
软件安装器 winget(系统自带,或在 Microsoft Store 更新「应用安装程序」) Homebrew(见 6.2) apt(系统自带) winget --version / brew --version
Git winget install --id Git.Git -e --source winget brew install git sudo apt install -y git git --version
Node.js LTS winget install --id OpenJS.NodeJS.LTS -e --source winget 或 nvm-windows:nvm install lts + nvm use lts brew install node 或 nvm:nvm install --lts nvm:nvm install --lts node -v、npm -v
编辑器 winget install --id Microsoft.VisualStudioCode -e --source winget 或 winget install --id Anysphere.Cursor -e --source winget brew install --cask visual-studio-code 或 brew install --cask cursor 官网下载 正常打开即可
GitHub CLI winget install --id GitHub.cli -e --source winget brew install gh 见 gh 官方 Linux 安装说明 gh auth status
Cloudflare CLI(cf) npm install --global cf@1.0.0-beta.27 同左 同左 cf --version、cf auth whoami
Wrangler(部署工具) 无需安装,由 OpenNext 调用;阶段 3 运行一次 npx wrangler login 同左 同左 npx wrangler whoami

Git 换行符设置:Windows 运行 git config --global core.autocrlf true 与 git config --global core.longpaths true;Mac / Linux 运行 git config --global core.autocrlf input。

项目存放路径:Windows 存放于 C:\code\worship-songs;Mac 存放于 ~/code/worship-songs;Linux / WSL 存放于 ~/code/worship-songs。请勿保存在 OneDrive 或 iCloud 等云同步目录。

各系统常见问题:详见 6.10。


附录 B:完整项目文件清单

讲义各阶段均已提供参考代码。以下为项目目录结构,以及各文件新建或更新的阶段(标有 ★ 表示后续阶段会覆盖更新)。

给 AI 的参考资料(不用看)
text
worship-songs/
├── .env.local                       阶段 5(Firebase 配置,不上传 GitHub)
├── database.rules.json              阶段 10(Realtime Database 安全规则)
├── db/
│   ├── schema.sql                   阶段 4
│   └── seed.sql                     阶段 4
├── next.config.ts                   阶段 3
├── wrangler.jsonc                   阶段 4 ★ → 阶段 8 最终版(加 R2)
├── open-next.config.ts              阶段 3(migrate 自动生成,不用改)
├── cloudflare-env.d.ts              阶段 4 / 8(npm run cf-typegen 自动生成)
└── src/
    ├── lib/
    │   ├── firebase.ts              阶段 5
    │   ├── server.ts                阶段 5
    │   └── song-format.ts           阶段 6
    ├── components/
    │   ├── AuthProvider.tsx         阶段 5
    │   ├── RequireLogin.tsx         阶段 5
    │   ├── Nav.tsx                  阶段 5
    │   ├── SongRender.tsx           阶段 6
    │   ├── SongForm.tsx             阶段 6
    │   ├── Attachments.tsx          阶段 8
    │   └── Comments.tsx             阶段 10
    └── app/
        ├── layout.tsx               阶段 5
        ├── globals.css              阶段 5(含打印样式,阶段 9 讲解)
        ├── page.tsx                 阶段 6(歌曲库)
        ├── login/page.tsx           阶段 5
        ├── admin/page.tsx           阶段 7
        ├── songs/
        │   ├── new/page.tsx         阶段 6
        │   └── [id]/
        │       ├── page.tsx         阶段 6 ★ → 阶段 8 加附件 → 阶段 10 最终版
        │       └── edit/page.tsx    阶段 6
        └── api/
            ├── me/route.ts          阶段 5
            ├── users/route.ts       阶段 7
            ├── files/[...key]/route.ts      阶段 8
            └── songs/
                ├── route.ts                 阶段 6
                └── [id]/
                    ├── route.ts             阶段 6 ★ → 阶段 8 最终版
                    └── attachments/route.ts 阶段 8

常用命令汇总:

命令 做什么
npm run dev 启动本地开发服务器(http://localhost:3000),使用本地数据库
npm run preview 在本地 Cloudflare 模拟环境中预览(http://localhost:8787)
npm run deploy 构建并部署至 Cloudflare
npm run cf-typegen 修改 wrangler.jsonc 后重新生成类型定义文件
npx wrangler d1 execute worship-db --local --command="SQL语句" 在本地数据库执行 SQL 语句(本地数据库由 Wrangler 管理)
cf d1 query 【你的database_id】 --sql "SQL语句" 在线上数据库执行 SQL 语句
cf d1 migrations apply 【你的database_id】 --dir db 将 db 目录下尚未执行的 .sql 文件导入线上数据库
cf d1 list 查看当前账号下的 D1 数据库列表及 uuid(即 database_id)
cf r2 buckets list 查看当前账号下的 R2 存储桶列表
cf auth whoami 查看 cf 登录状态
git add -A → git commit -m "说明" → git push 提交当前代码并推送至 GitHub
git log --oneline 查看历史提交记录
git restore . 撤销当前未提交的修改

附录 C:官方文档链接

AI 编程工具

环境安装

Next.js 与 Cloudflare

Firebase


讲义版本:2026 年 11 月。若实际操作时发现步骤与网站最新界面存在差异,请以官方文档为准,并可向老师反馈以便更新讲义。

AI 入门实战训练营 · 敬拜团歌谱编辑系统

构建于:2026/11/05 14:35:49 (SGT)