交互课件开发总指南

Quarto Reveal.js 的设计、资源、交互与迭代规范

系统说明 slide-reactive 交互课件的设计架构、资源分层、CSS 风格、JavaScript 与 R/Plotly 交互、验证流程和项目最佳实践。
课堂互动
Reveal.js
Quarto
开发规范
作者

胡华平

发布于

2026年9月29日

修改于

2026年9月29日

摘要
本文是 slide/slide-reactive 的统领性开发文档。它说明公共配置与讲义专用资源如何分工,梳理 title、H1、H2、end slide 的视觉规范,总结投票图、引用跳转、选择题和 Plotly 动画的实现方式,并给出新增课件、维护旧课件和发布前检查的标准流程。

本文记录 slide/slide-reactive/ 交互课件经过多轮开发后形成的设计原则和工程约定。它不是某一个互动功能的源码讲解,而是整个课件体系的入口文档:先回答“文件放在哪里、配置写在哪里、各层负责什么”,再说明“怎样新增、验证和维护一份课件”。

投票图与引用跳转的逐行 JavaScript 说明,参见《用 JavaScript 做 Reveal.js 课堂互动》。本文只保留其架构结论和开发接口,不重复解释每一行脚本。

1 设计目标

交互课件同时服务课堂展示、现场操作和后续人工审核,因此需要满足五个目标:

  1. 一致:title slide、章节页、普通标题和结束页保持统一视觉语言。
  2. 专用:一份讲义独有的 CSS、脚本、数据和图片能够快速定位。
  3. 共享:真正跨课件复用的配置和样式只维护一份。
  4. 可复现:R 模拟、图形和默认交互状态能够由固定代码重新生成。
  5. 可验证:渲染成功不等于功能正确,必须在浏览器中检查动态行为。

这里最重要的判断不是“CSS 都放一起、JavaScript 都放一起”,而是按责任和作用域组织资源。文件类型相同,不代表生命周期相同。

2 当前课件体系

活动入口 slide/slide-reactive/list-activities.qmd 当前连接四份互动讲义:

  • chpt01-em-language.qmd:计量经济学语言;包含课堂选择题和章节页。
  • chpt01-act-em-schools.qmd:客观世界与思维模型中文稿;包含投票图和文内引用导航。
  • chpt01-act-em-schools-en.qmd:上一份讲义的英文版本;共用同一套专用资源。
  • chpt10-multicollinearity.qmd:多重共线性;由 R 生成数据和 Plotly 图形,JavaScript 负责播放与尺寸协调。

chpt00-revealjs-basic.qmd 是 Reveal.js 基础示例,没有接入 _reveal.yml,不应把它误当作当前互动课件规范的实现样板。

2.1 每套讲义的专用资源

slide/slide-reactive/
├── _metadata.yml
├── _reveal.yml
├── list-activities.qmd
├── scss/
│   ├── reveal-common.css
│   ├── title-slide.html
│   └── title-slide.scss
├── chpt01-em-language.qmd
├── chpt01-em-language/
│   ├── slides.css
│   ├── interaction.html
│   └── chpt11-mroz2-eq-2sls-adj.png
├── chpt01-act-em-schools.qmd
├── chpt01-act-em-schools-en.qmd
├── chpt01-act-em-schools/
│   ├── slides.css
│   ├── poll-chart.html
│   └── cite-nav.html
├── chpt10-multicollinearity.qmd
└── chpt10-multicollinearity/
    ├── slides.css
    ├── R/
    │   ├── mc-data.R
    │   └── mc-plotly.R
    └── js/
        └── plotly-interaction.html

目录树表达了两个层级:

  • scss/ 和 _reveal.yml 是公共层。
  • 与主 QMD 同名的文件夹是讲义专用层。

中英文版本不是两套独立产品。chpt01-act-em-schools.qmd 与 chpt01-act-em-schools-en.qmd 属于同一课件家族,因此共同使用 chpt01-act-em-schools/。

3 从源文件到课堂页面

flowchart LR
  qmd["QMD:结构与教学内容"]
  metadata["_reveal.yml:公共元数据"]
  deckCss["同名目录:专用 CSS"]
  behavior["HTML/JS:交互行为"]
  rCode["R:数据与 Plotly 图形"]
  quarto["Quarto + knitr + Pandoc"]
  reveal["Reveal.js HTML"]
  browser["浏览器课堂运行"]

  metadata --> quarto
  qmd --> quarto
  deckCss --> quarto
  rCode --> quarto
  behavior --> quarto
  quarto --> reveal
  reveal --> browser

各层只承担一种主要职责:

  • QMD:标题层级、正文、原生 HTML 标记、课堂说明和 speaker notes。
  • YAML:格式、资源入口、作者信息和渲染参数。
  • CSS/SCSS:布局和视觉,不计算业务状态。
  • JavaScript:事件、状态变化、Reveal API 和 Plotly 浏览器行为。
  • R:可复现数据、统计模型和渲染前图形构造。

把计算、外观和行为混进同一个文件,短期看似方便,长期会使审核者无法判断某次修改影响哪一层。

4 配置继承与视觉系统

4.1 四层配置链

互动课件不是只受 QMD 顶部 YAML 控制。实际配置从外到内依次为:

  1. 项目根 _quarto.yml:网站类型、_site 输出目录、全局语言、bibliography、freeze: auto 和 post-render;
  2. slide/slide-reactive/_metadata.yml:该目录默认隐藏代码回显;
  3. 讲义通过 metadata-files 引入的 _reveal.yml:互动课件公共 Reveal 配置;
  4. 单份 QMD:标题、footer、专用 CSS/脚本和个别导航选项。

越靠近文档的设置越具体。list-activities.qmd 显式使用 format: html,所以它是网站列表页而不是 Reveal deck。不要把 include-after-body 放入目录 _metadata.yml 或 _reveal.yml,否则列表页和无关讲义可能加载不属于自己的脚本。

4.2 公共 YAML

每份规范化互动课件通过下面的元数据入口继承公共配置:

metadata-files:
  - _reveal.yml

_reveal.yml 负责:

  • 1600 × 900 画布、页边距与 auto-stretch;
  • 公共日期、中文作者和单位;
  • title slide 背景;
  • Logo、页码、控制器和 chalkboard;
  • title-slide.html 与 title-slide.scss;
  • reveal-common.css。

讲义 QMD 只保留自己的 title、subtitle、footer 和专用资源。英文稿在本地覆盖 lang、英文作者和英文单位,但继续继承背景、画布和公共主题。

这一做法遵循“默认值集中,差异值就近”原则。不要把每份讲义都相同的作者块和背景参数复制四遍。

4.3 CSS 分层

公共 CSS 位于 scss/reveal-common.css,只放多套不同课件都需要的规则:

  • H2 标题颜色和浅蓝下边线;
  • H1 章节页样式库;
  • .center-h 结束页基础行为;
  • Logo、footer 和 slide number。

专用 CSS 位于同名目录的 slides.css:

  • .eml-*、.mcq 属于计量经济学语言课件;
  • .quiz-*、.poll-*、.stage-*、.cite-* 属于学派互动课件;
  • .lift-split、.plane-tall、.tf-slide 属于多重共线性课件。

判定一条规则是否公共时,按“课件家族”而不是按 QMD 数量计算。中英文稿同时使用 .poll-layout,仍然只算一个课件家族,因此它是专用样式。

CSS 选择器应以 .reveal 开头,减少对普通网站页面的污染。专用类还应带稳定前缀,例如 eml-、poll- 或 mc-,不要创建含义模糊的 .box、.left、.blue。

4.4 title slide

title slide 由三部分共同完成:

  • _reveal.yml 提供元数据和背景参数;
  • scss/title-slide.html 决定标题、作者、单位和日期的 HTML 结构;
  • scss/title-slide.scss 调整作者区布局。

修改 title slide 前先判断需求属于“数据、结构还是外观”,分别改 YAML、HTML partial 或 SCSS。不要为了移动作者姓名而修改每份 QMD。

4.5 H1 章节页

H1 用于纯章节分隔页。公共样式提供一个基础类和四个变体:

# 第一部分 {.section-title .section-title--academic}

可用变体:

  • .section-title--academic:学术蓝渐变和细网格,当前默认。
  • .section-title--image:共用封面图叠加深色遮罩。
  • .section-title--minimal:浅色留白和左侧强调条。
  • .section-title--spotlight:深蓝背景和柔和光晕。

每张 H1 页使用一个变体即可。变体负责视觉差异,.section-title 负责共同布局。不要复制整段 CSS 创建“几乎一样”的第五种风格。

4.6 H2 普通页面

H2 是日常内容页标题,由公共 CSS 自动添加浅蓝线条。QMD 不需要逐页加 class:

## 四句话,四个身份

只有明确的特殊页面才能覆盖这一规则。覆盖应放在该讲义的 slides.css,并以页面 class 限定作用域。

4.7 end slide

结束页只保留一个 H2,不再叠加讲义专用正文:

## 互动练习结束{.center-h background-color="aliceblue" background-image="../pic/thank-you-gif-funny-fan.gif" background-size="600px" background-position="center" background-opacity="0.8" style="font-size:65px !important"}

英文稿可以本地化标题文字,结构与视觉参数保持一致。公共 CSS 会在 .center-h 页面取消 H2 下边线。

5 专用资源管理

5.1 文件夹命名

主文件 name.qmd 对应 name/。语言后缀不参与资源文件夹命名:

chpt01-act-em-schools.qmd
chpt01-act-em-schools-en.qmd
chpt01-act-em-schools/

文件少时可直接放在目录根部;资源类型变多后再使用 R/、js/ 等子目录。不要预先创建一批空目录。

5.2 哪些资源必须专用

以下内容只要服务单一课件家族,就进入同名目录:

  • include-after-body 引入的 HTML/JavaScript;
  • 讲义专用 slides.css;
  • R helper;
  • 数据文件;
  • 只在该课件出现的图片;
  • 与该课件一起维护的说明或测试素材。

不要建立目录级公共 js/ 来收纳互不相关的脚本。脚本是否公共由业务行为决定,而不是由扩展名决定。

5.3 哪些资源可以公共

只有出现明确跨课件需求时,资源才进入公共层:

  • 统一 title partial 和主题;
  • 全部互动讲义的画布与导航配置;
  • 多套课件共同使用的标题、Logo、footer 和页码样式;
  • 多套讲义真正共用的 slide/pic/ 图片。

“未来也许会复用”不是公共化理由。先专用,出现第二个独立课件家族后再抽取。

6 交互实现技术栈

6.1 纯 JavaScript 与 HTML 标记

轻量交互使用浏览器原生 API,不引入额外前端框架。主要技术包括:

  • querySelector / querySelectorAll;
  • dataset 和 data-* 协议;
  • addEventListener;
  • classList;
  • SVG DOM;
  • Reveal.on、Reveal.getIndices、Reveal.slide;
  • Plotly 浏览器 API。

HTML class 服务 CSS,data-* 服务 JavaScript。不要让脚本通过中文显示文本猜测业务含义。

脚本通常用 IIFE 隔离变量:

<script>
(function () {
  // deck-specific behavior
})();
</script>

若脚本依赖页面 DOM,应等待 DOMContentLoaded;若依赖 Reveal 当前页尺寸,还应监听 ready 和 slidechanged。

6.2 投票柱状图

chpt01-act-em-schools/poll-chart.html 使用原生 SVG:

  1. 从 .poll-input[data-poll] 读取人数;
  2. 校验并合计数值;
  3. 清空旧 SVG;
  4. 重画网格、柱子、数字和标签;
  5. 在输入变化、Reveal ready 和 slidechanged 时重新绘制。

脚本按 .poll-layout 局部查询节点,因此一页存在多个互动区域时不会串值。英文稿通过 data-poll-lang="en" 切换标签,而不是复制一份英文脚本。

6.3 引用跳转

cite-nav.html 通过 data-cite-nav 建立正文编号和参考资料之间的双向连接。普通 href="#id" 不足以驱动 Reveal 翻页,因此脚本先找到目标所属的 <section>,再调用 Reveal API。

事件监听放在 document 上,属于事件委托。它能处理后续生成的节点,也减少重复 listener。

6.4 选择题即时反馈

chpt01-em-language/interaction.html 查找每个 .mcq:

  • data-answer 保存正确选项;
  • data-explain 保存正确解释;
  • 按钮的 data-choice 表示作答;
  • is-correct / is-wrong class 控制视觉状态;
  • aria-pressed 和 aria-live 提供基础无障碍反馈。

题干和答案属于教学内容,应留在 QMD;脚本只实现通用判定过程。

6.5 R、knitr 与 Plotly

多重共线性课件把统计计算放在 R:

  • mc-data.R 使用固定种子、MASS::mvrnorm、lm 和 stats 生成可复现样本与模型结果;
  • mc-plotly.R 使用 plotly 构建二维、三维和动画图形;
  • htmlwidgets::onRender 把必要配置挂到浏览器图形节点;
  • plotly-interaction.html 负责播放、暂停、翻页停止和尺寸重算。

统计机制与早期可视化探索参见《多重共线性问题》;本指南关注现行课件的工程实现。

R 负责“数据和图是什么”,JavaScript 负责“浏览器里怎样操作”。不要在 JavaScript 中重新实现统计模拟,也不要为一次点击重新运行 R。

三维图页使用 data-prevent-swipe="true",避免拖动图形时被 Reveal 解释成翻页手势。

6.6 R 环境与 freeze

仓库使用 renv.lock 固定 R 依赖;首次在新环境渲染前,应先恢复项目环境:

renv::restore()

项目设置 execute.freeze: auto。目前四份互动讲义中,只有多重共线性课件执行 R,因此修改 mc-data.R、mc-plotly.R 或相关 chunk 后,需要确认 _freeze/slide/slide-reactive/chpt10-multicollinearity/ 是否应随源代码更新。

普通 CSS、JavaScript 或文字修改不应无差别提交其他文章的 freeze 变化。若缓存结果明显未刷新,可显式刷新该讲义:

quarto render "slide/slide-reactive/chpt10-multicollinearity.qmd" --cache-refresh

7 标准开发流程

7.1 定义课堂行为

先写清楚:

  • 学生要做什么;
  • 教师何时操作;
  • 页面状态怎样变化;
  • 是否需要中英文;
  • 是否需要可复现数据。

不要从“我要写一个脚本”开始。交互代码是课堂流程的实现,不是设计目标。

7.2 创建主文件和同名目录

新课件建议从最小头部开始:

---
title: "课件标题"
subtitle: "课件副标题"
metadata-files:
  - _reveal.yml
format:
  revealjs:
    footer: "章节|主题"
    css: chptXX-topic/slides.css
    include-after-body: chptXX-topic/interaction.html
---

没有专用 CSS 或 JavaScript 时不要写空配置。

7.3 先完成静态结构

先让 QMD 在没有 JavaScript 时仍能表达问题和结论。为交互区域选择稳定 class,并用 data-* 写机器可读的键。

7.4 添加专用视觉

只在 name/slides.css 中写本讲义规则。确认相同需求已经存在于其他课件后,才考虑移动到 reveal-common.css。

7.5 添加行为

脚本必须:

  • 限定查询根节点;
  • 对缺失节点安全退出;
  • 不污染全局变量,除非确实要暴露给 Plotly;
  • 不依赖页面显示文字;
  • 处理 Reveal ready / slidechanged;
  • 更新可访问状态。

7.6 添加可复现计算

R 模拟应固定 seed,并将数据生成、模型拟合和绘图函数分开。QMD 中只保留调用顺序和教学内容,复杂函数进入 R/。

7.7 更新活动入口

新讲义确认可用后,再加入 list-activities.qmd。入口链接到 QMD,让 Quarto 建立项目依赖和最终 HTML 链接。

7.8 单独渲染

在项目根目录执行:

quarto render "slide/slide-reactive/chptXX-topic.qmd"

一次修改多份课件时逐份渲染。这样可以明确是哪一份失败,也避免把目录中实验稿一并构建。

全站发布使用:

quarto render

全站渲染结束后,项目会执行 R/copy-post-sync.R,把 _site 镜像到同步目录。单份讲义开发阶段不应把同步结果当作源文件修改。

8 验证与回归测试

8.1 静态检查

  • YAML 缩进正确;
  • 所有资源路径存在;
  • 生成 HTML 同时加载 scss/reveal-common.css 和专用 slides.css;
  • 页面没有引用已移动的旧路径;
  • CSS 选择器没有泄漏到无关讲义;
  • 浏览器控制台没有 JavaScript 错误。

8.2 视觉检查

  • title slide 的背景、作者、单位和日期正确;
  • H1 章节页铺满课件画布,文字对比度足够;
  • H2 有统一浅蓝线;
  • end slide 只有标题,没有残留正文;
  • Logo、footer 和页码不遮挡内容;
  • 1600 × 900 下不溢出;
  • 缩小窗口后仍能阅读。

8.3 功能检查

学派互动课件:

  • 改一个人数后,合计和柱高同时变化;
  • 中英文标签正确;
  • 点击引用编号进入参考资料;
  • 点击“返回 / Back”回到正文并短暂高亮。

计量经济学语言课件:

  • 点击答案后只保留一个按钮状态;
  • 正确与错误反馈文字、颜色和 aria-pressed 正确。

多重共线性课件:

  • Plotly 能旋转、缩放和选择样本;
  • Play / Pause 正常;
  • 翻页后计时器停止;
  • 重新进入页面后图形尺寸正确;
  • 拖动三维图不会触发 Reveal 翻页。

8.4 本地浏览器检查

渲染后可临时启动静态服务器:

python -m http.server 8765 --directory _site

然后打开:

http://127.0.0.1:8765/slide/slide-reactive/chptXX-topic.html

不要只用 file:// 检查;资源路径、模块策略和浏览器安全限制可能与 HTTP 环境不同。

8.5 PDF 与离线边界

公共配置当前使用 embed-resources: false,因此互动课件依赖 _site 中的 CSS、图片和脚本,不是可随意单独复制的自包含 HTML。课堂使用应发布完整站点目录。公式离线排版见《全站数学公式的本地 MathJax》。

chpt00-revealjs-basic.qmd 及 topics-reveal.qmd 保留了 Chrome 与 decktape 的 PDF 对照材料,可用于导出方法参考;它们是基础实验,不是新互动课件的配置模板。PDF 无法完整保留输入框、按钮、动画和引用跳转,发布前应把“可打印讲义”和“课堂交互 HTML”视为两种不同产物。

9 版本控制与迭代规范

9.1 小步提交

一次提交只解决一个明确问题,例如:

  • 归位一套课件资源;
  • 抽取公共 CSS;
  • 新增一个互动;
  • 统一 title / heading / end slide;
  • 修复 Plotly 翻页尺寸。

资源移动与功能重写最好分开提交,便于审核路径变化和行为变化。

9.2 精确暂存

Quarto 渲染可能修改 _freeze/ 或生成主题文件。提交前使用 git status 和 staged diff,只暂存本次源文件。除非明确要刷新冻结结果,否则不要顺手提交无关 _freeze 变化。

数据文件、图片和旧稿也不能因为“在同一目录”就自动加入提交。先确认它们被当前 QMD 引用。

9.3 修改后的最小回归集合

  • 改 _reveal.yml、title partial 或 reveal-common.css:渲染四份规范化互动课件。
  • 改某个 name/slides.css:渲染该课件家族的全部语言版本。
  • 改共享脚本:检查所有使用该脚本的语言版本。
  • 改 R helper:重新渲染多重共线性课件并操作关键 Plotly 页。
  • 只改教学文字:至少渲染该 QMD,检查分页和溢出。

10 常见反模式

10.1 把专用规则放进公共 CSS

后果是所有课件都下载无关样式,并且同名 class 容易发生覆盖。解决办法是搜索 class 实际使用位置,按课件家族判定归属。

10.2 建立目录级公共 JavaScript 杂物箱

js/ 看起来整齐,却失去“这个脚本属于哪份课件”的信息。专用脚本应跟随同名目录。

10.3 中英文复制两份逻辑

复制会让修复只落在一个版本。应共用脚本,用 lang 或 data-* 提供语言差异。

10.4 通过可见文字驱动逻辑

文本翻译、标点或排版变化都会让脚本失效。应用稳定的 data-choice、data-poll、data-cite-nav。

10.5 只看渲染退出码

Quarto 成功只能证明生成了 HTML,不能证明柱状图画出、引用能翻页、MCQ 能反馈或 Plotly 能 resize。

10.6 把所有内容塞进 QMD

长 CSS、复杂 R 函数和完整 JavaScript 混在正文中会妨碍教学审核。QMD 应尽量呈现“课堂讲什么”,专用资源文件呈现“页面怎样工作”。

10.7 未验证就抽象

只有一个课件使用的规则先保持专用。过早抽象会产生难懂的全局配置和兼容负担。

11 Cursor 项目规范

项目当前的相关规则是:

.cursor/rules/reactive-slide-resources.mdc

规则会在编辑 slide/slide-reactive 下的 QMD、HTML、CSS、SCSS、YAML 和 R 文件时提供上下文,核心要求包括:

  • 公共层与专用层分离;
  • 语言版本共用同名目录;
  • 公共 title、H1、H2、end slide 规范;
  • 移动资源后同步更新讲义、语言版本和技术笔记;
  • 修改后渲染并核对公共 CSS 与专用 CSS。

项目目前没有 .cursor/skills/ 下的项目级 skill。因此,Cursor rule 是自动生效的约束,本文是供人和 Agent 阅读的完整解释。如果以后形成稳定、重复且需要执行命令的自动化流程,例如“创建新互动课件骨架并完成浏览器验收”,再把该流程实现为项目 skill;不要用 skill 重复存放本文全部知识。

12 维护原则

今后的修改应始终回答三个问题:

  1. 这是内容、外观、行为还是数据问题?
  2. 它服务一套课件,还是至少两个独立课件家族?
  3. 怎样证明修改在浏览器课堂环境中有效?

只要坚持“职责分层、资源就近、差异显式、渲染加浏览器验证”,slide-reactive 就能在增加课件数量和交互复杂度后,仍然保持可审核、可维护和可复用。