SCORM课件演示与测试全解析:从API通信到自动化验证
简介这是一套遵循SCORM标准的交互式课件演示与测试资源面向E-Learning课程开发者、LMS平台测试人员及在线教学从业者用于验证课件在不同学习管理系统中的识别、传递与跟踪能力。压缩包共28个文件约12.07MB以swf动画、htm页面、xsd与xml结构定义、js脚本为主swf承载多媒体课程主体htm对应各章节学习单元xml与xsd负责课程结构与元数据规范js支撑交互与工具逻辑。资源将课程拆分为多个可独立复用的学习单元配合清单文件描述课程组织与导航策略便于平台解析运行同时内置评估与反馈机制学习者可按节奏探索内容并自我检测教师或管理员则能获取进度与表现数据。已有579人学习下载适合作为SCORM标准落地、课件兼容性测试与富媒体课程结构分析的参考样本。1. 从一份“打不开”的课件说起SCORM标准课件演示与测试到底在做什么如果你做过企业培训、在线教育或者高校课程平台大概率遇到过这种场景课程包上传到平台后学员点开发现页面一片空白或者进度条卡在 0% 不动又或者学完了平台却显示“未完成”。这类问题十有八九出在 SCORM 标准课件的演示与测试环节。SCORMSharable Content Object Reference Model是一套让课件能在不同学习管理系统之间“即插即用”的规范核心是课件与平台之间通过一套固定的 JavaScript API 通信上报学习状态。演示与测试要做的就是验证这份通信链路是否通畅、数据是否准确、异常是否可控。适合谁看如果你是课程开发、平台运维、教学设计师或者需要交付课件包的工程师这篇笔记会从零讲清怎么搭测试环境、怎么跑通演示、参数怎么调、坑在哪。2. 拆解 SCORM 课件演示的通信链路从 API 发现到数据落库SCORM 课件演示不是“打开网页看看”那么简单。它本质是一次跨文档的 JavaScript 调用过程课件通常是 iframe 或弹窗需要找到平台提供的 API 实例然后调用LMSInitialize、LMSSetValue、LMSCommit、LMSFinish等方法。测试的核心就是确认这条链路每一步都按预期执行。2.1 课件如何找到平台的 API三种发现顺序与常见失败点SCORM 规范定义了 API 发现算法课件会按顺序在window、window.parent、window.top以及更上层框架中查找名为API或API_1484_11的对象。常见做法是递归向上遍历直到找到或到达顶层。很多“打不开”的课件就是因为平台把 API 挂在了错误的窗口层级或者 iframe 跨域导致访问被拦截。我一般会先写一个最小化的探测脚本在课件加载前注入打印每一层窗口是否存在 API 对象// 在课件页面加载前执行用于探测 API 所在层级 function findAPI(win) { let tries 0; // 最多向上查找 10 层避免死循环 while (win.API null win.parent ! null win.parent ! win tries 10) { tries; win win.parent; } return win.API || null; } // 从当前窗口开始查找 const api findAPI(window); if (api) { console.log(找到 API版本, api.LMSGetValue(cmi.core._version)); } else { console.error(未找到 API检查平台是否在父窗口暴露了 API 对象); }逻辑说明findAPI从当前窗口逐层向上直到找到API属性或到达顶层。参数tries限制查找深度防止某些嵌套框架导致无限循环。如果返回null说明平台没有在可访问的窗口层级挂载 API或者跨域策略阻止了访问。此时要检查平台初始化代码是否在正确的窗口执行以及 iframe 的sandbox属性是否允许脚本访问父窗口。2.2 初始化与数据读写LMSInitialize 到 LMSCommit 的完整调用序列找到 API 后课件必须调用LMSInitialize()开始会话。这一步会触发平台创建学习记录并返回true或false。很多测试只检查返回值却忽略了后续的数据写入顺序。正确的序列是初始化 → 读取初始状态如cmi.core.lesson_status→ 根据学习行为调用LMSSetValue写入 → 定期LMSCommit提交 → 结束时LMSFinish。下面是一个模拟课件上报学习进度的最小示例// 假设 api 已通过 findAPI 获取 const initResult api.LMSInitialize(); if (initResult ! true) { console.error(初始化失败错误码, api.LMSGetLastError()); return; } // 读取当前学习状态避免覆盖已有进度 const status api.LMSGetValue(cmi.core.lesson_status); console.log(当前状态, status); // 设置学员 ID 和位置 api.LMSSetValue(cmi.core.student_id, test_user_001); api.LMSSetValue(cmi.core.lesson_location, chapter_2); // 上报进度完成 50% api.LMSSetValue(cmi.core.score.raw, 50); api.LMSSetValue(cmi.core.lesson_status, incomplete); // 提交数据到平台 const commitResult api.LMSCommit(); if (commitResult ! true) { console.error(提交失败错误码, api.LMSGetLastError()); } // 结束会话 api.LMSFinish();逻辑说明LMSInitialize的参数通常为空字符串部分平台要求传入。LMSSetValue的第一个参数是 CMI 数据模型元素必须严格按 SCORM 版本1.2 或 2004的命名规则比如 1.2 用cmi.core.lesson_status2004 用cmi.completion_status。LMSCommit负责把内存中的数据刷到平台数据库不调用它平台可能只收到初始化记录。LMSFinish结束会话之后不能再调用LMSSetValue。参数说明cmi.core.score.raw是原始分数范围由cmi.core.score.min和cmi.core.score.max决定默认 0-100。cmi.core.lesson_status可选值包括passed、completed、failed、incomplete、browsed、not attempted。写错值会导致平台拒绝保存但 API 可能只返回false需要用LMSGetLastError和LMSGetErrorString获取详细错误。2.3 用本地模拟器跑通演示不依赖真实平台的测试方法真实平台往往有权限、网络、缓存等干扰测试阶段我习惯先用本地模拟器。常见做法是写一个简单的 HTML 页面在window上挂一个模拟 API 对象把所有调用和返回值打印到控制台或页面上。这样能快速验证课件的调用序列是否正确而不必反复上传课件包。!DOCTYPE html html headmeta charsetutf-8titleSCORM 模拟器/title/head body script // 模拟 API 对象记录所有调用 window.API { _data: {}, LMSInitialize: function() { console.log(LMSInitialize); return true; }, LMSGetValue: function(key) { console.log(LMSGetValue, key); return this._data[key] || ; }, LMSSetValue: function(key, val) { console.log(LMSSetValue, key, val); this._data[key] val; return true; }, LMSCommit: function() { console.log(LMSCommit, JSON.stringify(this._data)); return true; }, LMSFinish: function() { console.log(LMSFinish); return true; }, LMSGetLastError: function() { return 0; }, LMSGetErrorString: function(code) { return No error; } }; /script !-- 课件 iframe 放在这里 -- iframe srccourse.html width800 height600/iframe /body /html逻辑说明模拟器把 API 挂在顶层window课件通过findAPI向上查找即可命中。所有方法都返回true并打印日志方便观察调用顺序和参数。如果课件在 iframe 内需要确保 iframe 同源否则跨域会阻止访问window.API。测试时打开浏览器控制台就能看到完整的调用链。参数说明模拟器不校验数据模型所以即使课件写入了非法字段也会返回成功。正式测试时建议在模拟器中加入字段白名单校验比如只允许cmi.core.*和cmi.suspend_data这样能提前发现拼写错误。3. 测试用例设计覆盖正常流程、边界值与异常中断演示跑通只是第一步真正决定课件能否上线的是测试用例的覆盖度。SCORM 课件的测试不能只点一遍“下一步”要模拟各种真实学习行为中途关闭、网络断开、重复进入、分数越界、超长字段等。3.1 正常学习流程的 6 个必测检查点一个完整的正常流程至少包含以下检查点每个点都要确认平台数据库和界面显示一致检查点操作预期结果初始化打开课件API 返回 true平台创建学习记录状态读取首次进入lesson_status为not attempted或空进度上报完成第一章lesson_location更新suspend_data保存分数写入提交测验score.raw在 min/max 范围内提交触发 Commit平台数据库立即更新结束关闭课件LMSFinish返回 true状态变为completed或incomplete我一般会写一个自动化脚本用 Selenium 或 Playwright 驱动浏览器按顺序执行上述操作并在每一步后查询平台 API 或数据库验证。如果没有自动化条件至少手动执行一遍并截图记录。3.2 边界值测试分数越界、超长 suspend_data 与特殊字符边界值是最容易翻车的地方。比如cmi.core.score.raw设为150而max是100平台可能截断、报错或静默忽略。cmi.suspend_data通常有长度限制SCORM 1.2 建议 4096 字符2004 更大超长会导致提交失败。特殊字符如、、如果未转义可能破坏 XML 存储。测试时我会构造以下用例// 边界值测试用例 const testCases [ { key: cmi.core.score.raw, value: 150, desc: 分数超过最大值 }, { key: cmi.core.score.raw, value: -10, desc: 分数为负 }, { key: cmi.suspend_data, value: A.repeat(5000), desc: 超长挂起数据 }, { key: cmi.core.lesson_location, value: scriptalert(1)/script, desc: 特殊字符 }, { key: cmi.core.student_name, value: 张三\u0000李四, desc: 空字符 } ]; testCases.forEach(tc { const result api.LMSSetValue(tc.key, tc.value); console.log(tc.desc, 返回, result, 错误, api.LMSGetLastError()); });逻辑说明逐个写入边界值观察 API 返回值和错误码。如果返回false用LMSGetLastError获取错误码常见的有401未初始化、403元素未实现、405值超出范围。对于超长数据平台可能返回true但实际截断需要查询数据库确认。参数说明cmi.suspend_data用于保存课件自定义的挂起状态比如当前页码、答题记录。不同平台限制不同测试时以平台文档为准但建议课件端主动限制在 4000 字符以内。特殊字符建议用encodeURIComponent编码后再存入读取时解码。3.3 异常中断测试断网、关闭浏览器与重复初始化真实学员不会按剧本操作。断网时LMSCommit会失败课件是否有重试机制直接关闭浏览器LMSFinish没调用平台能否通过超时机制标记为 incomplete重复调用LMSInitialize会怎样这些都要测。我一般会模拟以下场景断网用浏览器开发者工具切到 offline触发一次LMSCommit观察返回值。如果返回false课件应缓存数据并在网络恢复后重试。强制关闭学习到一半直接关标签页重新打开课件检查cmi.core.lesson_location和suspend_data是否恢复。重复初始化在控制台手动再调一次LMSInitialize多数平台会返回false并报101已初始化错误。课件应避免重复调用。这些测试能暴露课件的健壮性问题。比如某次我遇到一个课件断网后LMSCommit失败但它没有重试导致学员后续的学习记录全部丢失。后来在课件里加了本地缓存和重试队列才解决。4. 避坑与排查SCORM 课件演示测试中最容易翻车的 5 个点4.1 现象课件显示“未找到 API”但平台其他课件正常原因课件在 iframe 内且 iframe 的sandbox属性没有包含allow-same-origin和allow-scripts导致无法访问父窗口的API对象。或者平台把 API 挂在了window.top但课件从window.parent开始查找中间某一层跨域中断了。解决检查 iframe 的sandbox配置确保允许同源和脚本。如果跨域不可避免平台需要把 API 通过postMessage代理但这不是 SCORM 标准做法需要改造课件。常见做法是让课件和平台同域部署或者用反向代理统一域名。4.2 现象LMSCommit 返回 true但平台数据库没有更新原因部分平台的LMSCommit是异步的返回true只表示调用成功不代表数据已落库。如果紧接着调用LMSFinish可能因为异步竞争导致数据丢失。另外某些平台只在LMSFinish时才真正写库。解决在LMSCommit后加一个短暂延迟如 200ms再继续或者调用平台提供的查询接口确认数据。更稳妥的做法是课件在关键节点多次LMSCommit并在LMSFinish前再提交一次。测试时直接查数据库不要只信返回值。4.3 现象lesson_status 设置为 completed 后平台仍显示“进行中”原因SCORM 1.2 和 2004 的状态字段不同。1.2 用cmi.core.lesson_status2004 用cmi.completion_status和cmi.success_status。如果课件按 1.2 写平台按 2004 解析就会读不到。另外某些平台要求先设置cmi.core.score.raw才能改状态。解决确认课件和平台使用的 SCORM 版本一致。在测试环境用LMSGetValue读取平台期望的字段名或者查看平台文档。如果不一致需要在课件端做版本适配或者让平台同时兼容两套字段。4.4 现象suspend_data 读取为空学习进度无法恢复原因suspend_data的写入和读取时机不对。有些课件在LMSInitialize后立即读取但平台可能还没把上次的数据加载到内存。或者课件在LMSFinish之后才写入此时会话已结束数据被丢弃。解决在LMSInitialize返回true后先调用LMSGetValue(cmi.suspend_data)如果为空再尝试等待 100ms 后重试。写入时确保在LMSFinish之前调用LMSCommit。测试时手动模拟“学一半关掉再打开”验证数据是否恢复。4.5 现象特殊字符导致提交失败错误码 405 或 201原因SCORM 数据通过 XML 或 URL 参数传输、、、%等字符未转义会破坏数据结构。某些平台对中文、emoji 支持不好也会报错。解决所有写入LMSSetValue的字符串先用encodeURIComponent编码读取时用decodeURIComponent解码。对于中文确保平台数据库字符集为 UTF-8。测试时专门构造包含特殊字符的用例观察错误码。如果平台不支持考虑用 Base64 编码后存入suspend_data。5. 进阶技巧用自动化脚本批量验证课件包与平台兼容性当你手上有几十个课件包或者平台要升级 SCORM 引擎时手动测试就不现实了。我一般会写一个 Node.js 脚本用 Puppeteer 启动无头浏览器加载模拟器页面然后依次注入不同的课件包自动执行一套标准测试用例最后输出兼容性报告。核心思路是模拟器页面暴露一个全局函数runTest(courseUrl)它动态创建 iframe 加载课件并监听 API 调用日志。脚本通过page.evaluate调用这个函数收集结果。// Node.js Puppeteer 批量测试脚本 const puppeteer require(puppeteer); async function testCourse(courseUrl) { const browser await puppeteer.launch({ headless: true }); const page await browser.newPage(); // 收集控制台日志 const logs []; page.on(console, msg logs.push(msg.text())); await page.goto(http://localhost:8080/simulator.html); // 注入课件并等待加载 await page.evaluate((url) { const iframe document.createElement(iframe); iframe.src url; iframe.id course-frame; document.body.appendChild(iframe); }, courseUrl); // 等待课件初始化完成 await page.waitForFunction(() { return window.API window.API._data window.API._data[cmi.core.lesson_status] ! undefined; }, { timeout: 10000 }).catch(() {}); // 获取模拟器记录的数据 const data await page.evaluate(() window.API._data); await browser.close(); return { courseUrl, data, logs }; } // 批量执行 const courses [course1.html, course2.html, course3.html]; (async () { for (const course of courses) { const result await testCourse(course); console.log(课件, result.courseUrl); console.log(最终状态, result.data[cmi.core.lesson_status]); console.log(错误日志, result.logs.filter(l l.includes(error))); } })();逻辑说明脚本启动无头浏览器打开本地模拟器页面然后动态插入 iframe 加载课件。模拟器的 API 对象会把所有写入的数据存到_data中脚本通过page.evaluate读取。waitForFunction等待课件完成初始化并写入至少一个状态字段超时则跳过。最后输出每个课件的最终状态和错误日志。参数说明headless: true表示无头模式适合 CI 环境。timeout: 10000是等待课件初始化的最长时间根据课件大小调整。logs收集控制台输出用于排查 API 调用错误。如果课件依赖用户交互如点击“开始学习”需要在脚本中模拟点击否则可能一直停在初始状态。这个脚本能快速筛出有问题的课件包比如未调用LMSInitialize、状态字段拼写错误、提交失败等。对于平台升级可以先用旧版模拟器跑一遍再用新版跑一遍对比数据差异提前发现兼容性问题。我自己的习惯是每次拿到新课件包先丢进这个自动化脚本跑一遍通过后再手动点几个关键流程。这样能省下大量重复劳动也能避免“以为没问题上线后学员反馈进度丢失”的尴尬。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

macOS/iOS高精度计时:mach_absolute_time原理与实战

macOS/iOS高精度计时:mach_absolute_time原理与实战

1. 项目概述:为什么mach_absolute_time是 macOS/iOS 时间测量的“黄金标准”在 macOS 和 iOS 开发中,我见过太多人用CFAbsoluteTimeGetCurrent()或NSDate.timeIntervalSinceReferenceDate做性能打点,结果在高精度场景下被系统时钟跳变、NTP 同…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →

最新新闻

尖叫声音样本数据集eva:从包结构到训练基线的完整实践

尖叫声音样本数据集eva:从包结构到训练基线的完整实践

简介:EVA尖叫声音样本数据集是一份面向音乐信息检索(MIR)与音频分类研究的人声尖叫样本库,由台湾清华大学MIR课程项目整理,旨在为尖叫检测、异常声音识别、SVM分类等实验提供稀缺的领域数据。压缩包共63个文件&#xf…

2026/10/14 0:47:55 阅读更多 →
现代C++多线程编程实践与优化指南

现代C++多线程编程实践与优化指南

1. 现代C多线程编程的核心价值十年前我刚接触多线程编程时,还在用pthread这类原始接口手动管理线程生命周期。现代C11标准引入的库彻底改变了游戏规则——现在我们可以用更安全、更直观的方式构建并发程序。举个例子,过去需要几十行代码实现的线程同步&a…

2026/10/14 0:47:55 阅读更多 →
Neo4j驱动的RAG:从向量检索到知识图谱推理的工程跃迁

Neo4j驱动的RAG:从向量检索到知识图谱推理的工程跃迁

1. 这份“日报”不是资讯汇编,而是工程实践的切片快照你点开这份标题为《Agent / LLM 技术精选日报 2026-09-28(知乎版)》的文档时,大概率会下意识把它当成一份轻量级技术资讯摘要——类似科技媒体每日推送的“今日AI圈发生了什么…

2026/10/14 0:47:55 阅读更多 →
JVM元空间内存泄漏排查与防护实战指南

JVM元空间内存泄漏排查与防护实战指南

1. 面试题背后的技术深度剖析当这个面试题出现在阿里巴巴Java技术面试中时,它实际上考察的是候选人对JVM内存管理的系统化理解能力和复杂问题排查经验。元空间(Metaspace)内存泄漏不同于传统的堆内存问题,它的隐蔽性更强且往往在应用运行较长时间后才显现…

2026/10/14 0:47:55 阅读更多 →
老虎数据集2055张VOC+YOLO格式:目标检测训练与标注避坑指南

老虎数据集2055张VOC+YOLO格式:目标检测训练与标注避坑指南

简介:本资源为面向目标检测学习者的老虎数据集,采用Pascal VOC与YOLO双格式标注,适合从事单类别目标检测训练、模型验证及算法对比的研究人员与开发者使用。压缩包共约2000个文件,以1999个xml标注文件和1个说明txt为主&#xff0c…

2026/10/14 0:47:55 阅读更多 →
开题报告是研究旅程的导航图,科迅捷AI帮你把路线画清楚

开题报告是研究旅程的导航图,科迅捷AI帮你把路线画清楚

如果把博士阶段的研究比作一段长途旅行,开题报告就是出发前画好的导航图。导航图不决定你路上的每一个细节,但它决定了方向:往哪走、经过哪些站点、预计什么时候到达。没有这张图,你很容易在路上迷路,或者走很远才发现…

2026/10/14 0:46:55 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →