# 学习通页面事实集(实测)
> 这份文档记录的是**实测确认过的**事实,不是猜的、不是文档抄的。
> 每一条都标注了它是怎么来的。
>
> **为什么要写它**:这些隐性知识过去散在代码注释里、散在我脑子里,
> 每次重构都要重新踩一遍。写下来之后,人和大模型都能直接用。
---
## 零、最重要的一条
### 完成状态只看 `aria-label`
```html
← 权威标记在这里
```
**`ans-job-icon-clear` 这个类名跟"完成"没有任何关系**(它是样式类)。
这个坑我踩过,而且是**双向都会错**:
| 场景 | `clear` 类判断 | `aria-label` 实际 | 后果 |
|---|---|---|---|
| 马原 章节测验 | 无 clear | 任务点已完成 | **漏判** → 会重做已得 100 分的测验 |
| 医学英语 视频 | 有 clear | 任务点未完成 | **误判** → 视频全被跳过,进度 0/66 |
而且"马原那边验证通过"是**巧合** —— 用一次巧合去验证判据,等于没验证。
**正确的验证方式是:至少要有一个"已完成"样本和一个"未完成"样本对照。**
---
## 一、页面模型:课程 → 小节 → 页面(tab) → 任务点
```
课程(course)
└ 单元(unit) ← 侧栏可折叠分组
└ 小节(section) ← .chapter_item[id],id 里含 chapterId
└ 页面(tab) ← #prev_tab li,有 title + cardid
└ 任务点 ← [class*=ans-job-icon],**可以有多个!**
```
### ★ 一个页面可以有多个任务点
实测(医学英语 Unit 1 小节 2):
| tab | 任务点 |
|---|---|
| `Video 3` | 视频(已完成)+ 章节测验「Unit 1 video 3」4 题(未完成) |
| `Video 4` | 视频(已完成)+ 章节测验「Unit 1 video 4」3 题(未完成) |
| `Listening` | 音频(未完成)+ 听力练习 4 题(未完成) |
| `Discussion` | **0 个**(纯讨论帖,不计进度) |
**所以「切过去 → 处理一个 → 翻页」是错的。必须先枚举这一页的所有任务点。**
(这正是 `inventory()` 存在的理由)
### 侧栏徽章
`.chapter_item` 的文字末尾带一个数字(如 `①` `⑤`)或 ✔,代表**该小节剩余任务点数**。
实测可读,但它是**页面加载时**的快照,可能滞后。
**权威进度**永远是章节页头部的「**已完成任务点: X/Y**」。
(课程列表页的 `.btm-cover` 进度条**不可靠** —— 25 张卡片里只渲染 3~4 个;对应接口已 404)
---
## 二、frame 结构(三层,容易搞错)
```
主 frame: /mycourse/studentstudy?chapterId=...&courseId=...
├ 知识卡片: /mooc-ans/knowledge/cards?clazzid=...&knowledgeid=...&num=N
│ ← 任务点图标在这里;num 就是 tab 序号
├ 模块外壳: /ananas/modules/video|audio|work|pdf|insertbbs/index.html
│ ← **常常是空的!**(innerText 长度 0)
└ 模块内容: /mooc-ans/work/doHomeWorkNew ← 题目在这里
/mooc-ans/work/selectWorkQuestionYiPiYue ← 已批阅的回顾视图
/mooc-ans/bbscircle/chapter ← 讨论帖内容在这里
```
### 踩过的坑
**坑 1:`find(modules/work) ?? find(doHomeWorkNew)` —— `??` 永远不执行。**
因为外壳 frame 一定存在(只是空的),所以 `??` 永远轮不到后备分支。
读到空内容,报「没有题目」。**实测:`doHomeWorkNew` 有 471 字符,`modules/work` 是 0 字符。**
**坑 2:`mooc-ans/work/` 下不止一种页面。**
| 页面 | 含义 |
|---|---|
| `doHomeWorkNew` | 待作答(有提交按钮) |
| `selectWorkQuestionYiPiYue` | **已批阅的回顾视图**(有成绩「本次成绩100分」、「我的答案:A」,**没有提交按钮**) |
只认第一种,会把一个**已得满分**的测验报成「模块加载失败」。
---
## 三、切页面(tab):`cardId` 才是判据
### 怎么做
tab 的 `onclick` 是站点自己的函数,直接执行:
```js
changeDisplayContent(idx, total, chapterId, courseId, clazzid, '')
```
**合成鼠标点击 `.chapter_item` 无效**(必须调用站点自己的 `toOld()`)。
### 怎么判断"真的切过去了"
**等 `knowledge/cards` 里的 `#cardId` 变成目标 tab 的 cardid。**
实测时间线(点击「章节测验」后):
```
~500ms cards frame: num=1 cardId=1209451486 ← 已经切好了
模块 frame: video → work
~1500ms 出现 doHomeWorkNew
```
### 踩过的坑
旧实现只等"某个模块 frame 就绪"—— 而**旧的 video 模块本来就已就绪**,
于是函数立刻返回,读到的全是**上一个 tab 的陈旧数据**。
结果:`cx_scan` 把「章节测验」识别成了 `type: "video"`,时长还抄了视频的 1973 秒。
**这是"章节测验被完全漏掉"的直接原因。**
**判据必须是:`#cardId === 目标 cardid`,不是"某个模块能读"。**
---
## 四、题目 DOM
### 通用结构
```html
← 每道题
1 ← 题号
← ★ 题干被字体混淆
【多选题】马撰思撱义...
提交
```
### 题型的三种形态
| 题型 | 选项容器 | `data` 的值 | onclick |
|---|---|---|---|
| 单选题 | `li.before-after-radio` | `A` `B` `C` `D` | `addSingleChoice(this)` |
| 多选题 | `li.before-after-checkbox` | `A` `B` `C` `D` | `addMultipleChoice(this)` |
| **判断题** | `li.font-cxsecret.before-after` `role=radio` | **`true` / `false`** | `addChoice(this)` |
| **填空题** | **没有 li** | — | 是 `input[type=text]` / `textarea` |
**判断题的 `data` 是 `"true"/"false"` 而不是字母** —— 用字母匹配会一个都抓不到。
**填空题没有选项,只有输入框** —— 需要写值并触发 `input`/`change` 事件。
### 选中状态
**看 `aria-checked`**,不要看 class:
```
点击前: li[aria-checked=null] span.class = "num_option_dx choice411484618"
点击后: li[aria-checked=true] span.class = "num_option_dx choice411484618 check_answer_dx"
```
**坑**:用 `/cur|check|selected/` 匹配 className 判定"已选中"会**全部误判** ——
因为选项的基础类名就叫 `before-after-checkbox`,**里面含 "check"**。
结果:每个选项都被当成"已选中",一个都没点。
### ★ 题干有字体混淆
| | 内容 |
|---|---|
| **DOM 文本** | `马撰思撱义毳撴不撮分割的三个撱譒撯成部分是指()。` |
| **截图里** | `马克思主义理论不可分割的三个主要组成部分是指()。` |
混淆标记是 `.font-cxsecret`(自定义字体)。
**选项文字不受影响,正常可读。**
→ **要读题干,必须截图。** 这是硬约束,不是可以绕过的。
---
## 五、提交确认框
提交按钮点下去会弹确认框,**而确认框在主 frame 里,不在 work frame 里**:
```html
← 主 frame
确认提交?
提交 ← ★ 确认按钮
```
**坑**:用"找文字是『提交』的按钮"来点确认,会命中**表单底部那个提交按钮**,
结果确认框一直开着、提交根本没发生 —— 而代码看起来"成功"了。
**必须按 `#popok` 精确定位。**
---
## 六、播放器:两种形态
| 形态 | 位置 | 依赖 |
|---|---|---|
| **视频** | `/ananas/modules/video/index.html` | videojs(`.vjs-big-play-button`,`videojs.getPlayers()`) |
| **音频** | `/ananas/modules/audio/index_new.html` | **原生 `