鸿蒙化Flutter工程中clean_coverage覆盖率报告净化实践
自从开始把 Flutter 工程往鸿蒙生态迁移我遇到的一个很现实的坑就是代码覆盖率报告越来越“好看”但越来越没人敢信。原因不复杂flutter test --coverage生成的 lcov.info 默认会把.g.dart、.dart_tool、build 缓存、插件注册文件全部算进去报告里的数字漂亮得惊人可真要拿它去卡质量基线又完全站不住脚。clean_coverage 这个三方库就是专门用来给覆盖率报告“去水分”的而到了鸿蒙化场景这套过滤逻辑还不能直接照搬必须按照 HAP 工程的实际目录结构和编译产物重新设计规则。这篇文章我会把 clean_coverage 在鸿蒙化 Flutter 工程里的适配过程完整拆开从覆盖率报告是怎么产生干扰的、过滤规则该怎么设计到 CI 流水线里怎么闭环全部过一遍。适合正在做鸿蒙化 Flutter 应用、又被覆盖率指标折腾得头疼的团队参考。1. 背景与核心诉求为什么鸿蒙化 Flutter 工程需要“净化”覆盖率报告先说个实际场景。一个普通的 Flutter 项目跑完flutter test --coverage产物是coverage/lcov.info里面一行行记录着每个 Dart 文件的覆盖汇总。问题在于这个文件里有大量“噪音”.dart_tool/flutter_build/dart_plugin_registrant.dart这种编译期自动生成的文件、build/目录下的临时产物、json_serializable 生成的.g.dart序列化代码全都会被统计进覆盖率。测试代码根本没碰过它们但它们会拉低分母、抬高一种很隐蔽的“虚假覆盖率”——因为很多模块报告被稀释到了无法解读的程度。clean_coverage 做的事很简单读入 lcov.info按配置规则把不需要的源文件剔除重新生成一份干净的覆盖率报告。它本质上是给测试报告做一次“内容审核”只留下真正需要被评估的业务代码。1.1 clean_coverage 解决什么问题核心价值就三个字可信度。没有清洗的覆盖率报告在研发流程里是最容易被挑战的。开发者看一眼报告发现里面大半是生成代码第一反应就是“这指标是假的”接下来整个覆盖率制度都会失去约束力。clean_coverage 通过两层机制解决这个问题按文件路径排除直接过滤掉.dart_tool、build、*.g.dart等命名空间。按 import 语句排除通过源码头部 import 的正则匹配滤掉引用了特定依赖的文件块。这两层机制配合起来就能做到“只测真代码”的效果。官方文档里最常见的写法是维护一个.clean_coverage.yaml把需要剔除的路径模式和 import 模式都写进去之后在 CI 里反复调用覆盖流程就能稳定复现。1.2 鸿蒙化场景的特殊变量到了鸿蒙化工程这里需要处理的噪音比普通 Flutter 工程多不少。首先OpenHarmony 侧工程目录是ohos/它跟 Flutter 平台的 iOS 目录ios/和 Android 目录android/结构完全不同。真正影响覆盖率报告的是 Flutter 工程经过鸿蒙化工具链处理后的生成代码平台插件桥接层鸿蒙化 Flutter 工程通常要依赖flutter_ohos一类的基础库插件注册时会生成对应的generated_plugin_registrant.dart这类注册文件在每次构建时都可能变化测试中却基本不会真正走完所有插件通道。EventChannel、MethodChannel 的桥接实现很多工程会把 platform channel 的封装单独放在lib/platform/下这些文件里大量都是模板代码测试覆盖它们收益很低但不去掉会让报告数值失真。.g.dart文件鸿蒙化适配过程中不少团队会选择重新生成序列化代码比如 JSON 模型、freezed 产物这些文件必须从覆盖率统计里踢出去。flutter build 生成物build/、.dart_tool/、ephemeral/这些目录无论哪个平台都存在在鸿蒙工程里同样会混入报告。如果用默认配置直接套用报告里就会出现“鸿蒙特有代码没有被统计”“公共业务代码分母被生成文件稀释”两种并发问题。这才是这次适配的真正难点要针对鸿蒙工程的目录接入点定制出“对业务代码有效对生成代码免疫”的过滤策略。2. 适配前准备把覆盖率报告生成链路理清楚网上很多讲 clean_coverage 的文章上来就贴配置却不说前面几步的原因。我建议任何团队在动手适配前先把报告生成链路完整跑一遍搞清楚最终产出的 lcov.info 里每一类路径都是从哪来的。否则配置再怎么改都是盲调。先看一个标准的 Flutter 覆盖率生成流程flutter test --coverage # 产物coverage/lcov.info这一步底层实际是 Dart VM 的 coverage 机制在工作。flutter test会拉起一个带 coverage 采集能力的 VM跑完测试后把每个 Dart 源文件的执行次数、分支覆盖情况汇总成 LCOV 格式。这里最容易被忽略的问题是只要测试过程中 Dart VM 加载过某个文件这个文件就会被写进 lcov.info不管它是否属于被测业务模块。这也是为什么连编译缓存、插件注册代码都会出现在报告里。2.1 先跑通普通 Flutter 工程里的基线在引入鸿蒙化变量之前先把 clean_coverage 在普通 Flutter 工程里跑通这样后续排错时至少能区分问题是出在过滤配置还是出在鸿蒙工程特有的路径上。步骤很简单dev_dependencies: clean_coverage: ^2.0.0然后执行flutter pub get flutter test --coverage dart run clean_coverage --config clean_coverage.yamlclean_coverage.yaml里我一般这样写outputPath: coverage/clean_coverage.info importE: import:\\s*[\](?:package:logging|dart:io) ignoreE: .*(\\.g\\.dart|generated_plugin_registrant\\.dart|freezed\\.dart|\\.dart_tool\\/.*|build\\/.*).*跑完后拿coverage/clean_coverage.info去对接后续统计基线就建立了。这里的重点是能肉眼确认输出文件的行数相比原始 lcov.info 是否“明显变少”如果几乎没变化多半是配置里的正则表达式没匹配上早点暴露问题。2.2 鸿蒙化工程里报告生成链路有什么不同鸿蒙化 Flutter 工程里flutter test --coverage生成报告的机制没有变但一个关键的差异点是Dart 层的 coverage 不会采集到.ets或原生代码所以 HAP 安装包里真正新增的原生鸿蒙逻辑并不在 lcov.info 的考察范围内。这导致一个常见误解有人以为适配鸿蒙化之后只要考一下 flutter test 覆盖率就等于覆盖了 HAP 里的代码实际并不是。真正会出现在 lcov.info 里的鸿蒙相关文件主要有三类ohos/目录里的 Dart 文件比如桥接封装、平台能力抽象接口的 Dart 侧实现。鸿蒙插件自动生成的注册类、注解处理产物。适配层代码常见命名带_ohos、_harmony后缀的 platform implementation。把这三类文件识别出来比单纯套一个“通用 ignoreE”要可靠得多。这也是我觉得“鸿蒙化适配”这个词在覆盖率工具语境里指的是对报告统计口径的重新治理不只是让工具能跑起来那么浅层。3. 核心过滤规则设计识别该过滤和不该过滤的文件接过适配任务后我先做的一件事是把鸿蒙化工程里所有会被 lcov.info 收录的路径全部列出来看一遍目录结构再决定规则。一个典型鸿蒙化 Flutter 工程大致长这样my_app/ ├── lib/ │ ├── main.dart │ ├── pages/ │ ├── models/ │ └── platform/ │ ├── event_channel_impl.dart │ └── method_channel_impl.dart ├── ohos/ │ ├── entry/ │ └── flutter_ohos/ ├── test/ ├── coverage/ ├── build/ └── .dart_tool/如果工程里用了 json_serializable 或 freezedlib/models/下还会出现一堆.g.dart、.freezed.dart。这些生成文件有一个共同特征命名规范高度统一且内容不该由测试来背书。3.1 优先排除的三类“重量级噪音”第一类是编译生成物目录.dart_tool/、build/、ephemeral/无论什么平台都必须排除第二类是代码生成器产物.g.dart、.freezed.dart、.grpc.dart这类后缀第三类是自动注册文件generated_plugin_registrant.dart、dart_plugin_registrant.dart在鸿蒙化工程里尤其要检查ohos侧生成物是否被一起写入了报告。举一个我在真实工程里看到的例子coverage/lcov.info ... SF:lib/models/user_info.g.dart SF:.dart_tool/flutter_build/dart_plugin_registrant.dart SF:ohos/flutter_ohos_generated.dart第三种文件的命名在不同工程里差别很大不能只靠后缀判断这也是为什么建议把工程目录结构化地看一遍。做过滤规则时用路径完整匹配比用宽泛正则更安全。3.2 过滤规则的两种写法及适用场景clean_coverage 的配置支持两个维度。一个是ignoreE直接针对 lcov.info 里的SF:字段做正则匹配适合按文件名、目录路径排除。另一个是importE需要读取源码中的 import 语句适合排除“凡是引用了某个库就整体不统计”的场景。举个例子。工程里有一个统一封装的network_client.dart它内部会引用dart:io的 HttpClient测试用例都用 mock 替代网络层统计它意义不大。这时用 importE 把它引用的标记性依赖写进去这一整类文件都会被排除。我的经验是默认规则里ignoreE的优先级最高能覆盖掉绝大多数噪音importE只在碰到“按依赖关系排除”的需求时再加以免规则过度复杂后续没人敢改。3.3 鸿蒙化场景的专属过滤建议在鸿蒙化适配中建议在通用规则之外增加下面几条ignoreE: | (.*\.dart_tool/.*) |(.*/build/.*) |(.*generated_plugin_registrant\.dart.*) |(.*\.g\.dart.*) |(.*\.freezed\.dart.*) |(.*ohos.*generated.*)特别提醒一点ohos/目录下不只有生成物还可能有我们手写的桥接入口。如果桥上入口没有覆盖测试但被统计进分母覆盖率会被拉得很低如果完全不统计又会缺少对桥接层的关注。我的建议是——不要一把梭把整个 ohos 目录排掉仅仅排除明确是生成物的文件然后把桥接层的联动测试纳入集成测试阶段不要混在单元测试覆盖率里算。4. 实操流程从配置到 CI 闭环的完整动作配置不是写出来就完事了整个适配流程需要反复跑、反复对比确认过滤效果符合预期。下面按操作顺序把全过程过一遍。4.1 三个核心指令搭建过滤工具链在鸿蒙化 Flutter 工程的pubspec.yaml中打入依赖dev_dependencies: clean_coverage: ^2.0.0执行安装flutter pub get第一次使用时在工程根目录创建.clean_coverage.yamloutputPath: coverage/clean_coverage.info importE: .*(?:package:mockito|package:build_test).* ignoreE: | (.*\.dart_tool/.*) |(.*/build/.*) |(.*generated_plugin_registrant\.dart.*) |(.*\.g\.dart.*) |(.*\.freezed\.dart.*) |(.*/ohos/.*/generated/.*)然后执行过滤flutter test --coverage dart run clean_coverage --config .clean_coverage.yaml这里有个小技巧outputPath如果设成默认的coverage/clean_coverage.info后续 CI 里可以直接把它当作权威覆盖率报告来读取不必再保留原始文件。4.2 怎么判断过滤结果是否有效过滤完不是看一眼文件存在就算完事。我一般用两个指标验证比较原始 lcov.info 和 clean_coverage.info 的SF:记录条数。检查剩余记录里还有没有生成目录和.g.dart文件。在命令行里快速统计grep -c ^SF: coverage/lcov.info grep -c ^SF: coverage/clean_coverage.info grep SF: coverage/clean_coverage.info | grep -E \.dart_tool|\.g\.dart|build/ | head -20第一条命令如果显示原始报告有 500 条记录过滤后只剩 120 条而 120 条里不再有生成物说明规则生效了。如果过滤后数量变化很小优先检查正则的分隔符和转义有没有问题LCOV 用的路径是正斜杠不要写成反斜杠。另外强烈建议把过滤后的结果用任意覆盖率工具如 VS Code 插件 Coverage Gutters、SonarQube 的 LCOV 解析打开看一眼确认没有误伤业务代码。我曾经把models目录整目录都排除掉结果一个关键的实体类被踢了出去覆盖率直接从 75% 飙到 80%看着欢喜实际上是统计口径出了大问题。4.3 在 CI 流水线里闭环起来手动跑通之后要让这套逻辑在 CI 里自动执行。我惯用的做法是在 CI 脚本里增加一个覆盖率计算步骤flutter test --coverage dart run clean_coverage --config .clean_coverage.yaml python3 - EOF import re import sys lines open(coverage/clean_coverage.info).readlines() hit 0.0 found 0.0 for line in lines: if line.startswith(DA:): parts line[3:].split(,) if len(parts) 2: found 1 if parts[1] ! 0: hit 1 if found 0: print(fclean coverage: {hit/found*100:.2f}%) EOF这只是个参考脚本实际项目里可以把这个计算逻辑替换成你自己团队的覆盖率准入插件。关键是把阈值卡死在过滤后的报告上而不是原始 lcov.info 上否则 CI 上的覆盖率波动会因为噪音文件的增删而剧烈起伏毫无规律可言。我踩过的一个坑是在 CI 上先跑了全量测试再跑生成代码检查很多团队会接一个build_runner步骤结果生成代码跑完后又刷新了.g.dart文件导致下一次覆盖率过滤时正则匹配到的路径列表变了。后来把顺序固定为“先生成代码 → 再跑测试 → 再统计覆盖率”才彻底稳定下来。5. 常见问题与避坑记录适配 clean_coverage 的过程中有几个问题几乎是每个人都会撞上的专门列出来方便排查时直接对照。现象原因解决方案过滤后覆盖率基本没变正则没有匹配到 LCOV 路径格式用grep SF: coverage/lcov.info打印真实路径再调整 ignoreE过滤后业务代码被误伤ignoreE 规则写得过宽用完整目录前缀替代宽泛后缀匹配尽量缩短 exclude 路径不同机器上过滤结果不一致Windows 和 Linux 的反斜杠路径问题统一使用/作为路径分隔符在 CI 容器里标准化工作目录派生文件.freezed.dart没被过滤后缀匹配被转义字符影响使用.*\.freezed\.dart.*或直接匹配freezed关键字鸿蒙插件注册文件仍然出现插件名称不在通用规则覆盖范围把具体文件名加入 ignoreE例如.*plugin_registrant.*还有一个值得留意的点是 clean_coverage 对 lcov.info 中目录路径的解析依赖的是/分隔符。Windows 上的 Flutter 工程如果代码检出了\就可能产生空匹配。我自己在 Windows 开发机上适配过一次装了 Git for Windows 把 autocrlf 打开后路径里的分隔符被转换了好几次过滤报告反复波动。最后在 CI 里统一使用 Linux 容器执行过滤问题消失。另外如果工程同时存在integration_test/目录这些集成测试文件通常不应该进单元测试的覆盖率统计。clean_coverage 的规则里可以加一条ignoreE: | (.*/integration_test/.*)对接 HAP 构建场景时这会减少很多“为什么集成测试没覆盖到原生鸿蒙代码”的困惑因为它明确地让单元覆盖率只回答“单元层逻辑被验证了多少”而不是“整个包被验证了多少”。做个阶段性总结clean_coverage 不是灵丹妙药它只负责把报告里的噪声去掉。真正决定覆盖率指标有没有价值的是团队对统计口径的约定——哪些文件算业务代码、哪些算生成代码、单元测试覆盖率是否要覆盖平台桥接层这些最好在适配开始前就达成共识。鸿蒙化工程的适配核心是把报告清洗规则和目录结构强绑定而不是直接套用开源项目默认配置就算交差。我在实际工程里的体会是过滤规则写好后还要定期维护。随着 Flutter 版本升级、鸿蒙 SDK 迭代生成文件的后缀和目录结构都可能变化建议把.clean_coverage.yaml纳入评审并在 CI 里加一个“过滤结果变化幅度”的检查避免一次 SDK 升级后统计口径悄悄漂移。最后再分享一个小技巧过滤后的报告文件命名里带上日期或构建号比如coverage/clean_coverage_${BUILD_NUMBER}.info在追查历史版本覆盖率突然飙升或暴跌时会帮你省下大量对报告猜测的时间。

相关新闻

不是宇宙崩了,是人类知识崩了——从空圈容错视角看物理极限

不是宇宙崩了,是人类知识崩了——从空圈容错视角看物理极限

作者:Liaiyang66 元宝(合著) 交叉互验:豆包、DeepSeek、千问 系列:空圈容错理论跨界推演随笔引子:从时间落差说起 以中国历史为参照,中国文明存在了五千年。但真正让中国走入现代科学文明的&am…

2026/10/11 6:06:01 阅读更多 →
太原网站建设哪家支持二次开发?

太原网站建设哪家支持二次开发?

不少太原企业搭建网站后,都会面临功能升级、系统对接、业务迭代的需求,因此选择支持二次开发的建站服务商至关重要。在太原本地建站机构中,科辉荣盛凭借成熟的技术体系和透明的服务模式,成为企业二次开发建站的优选品牌。深耕太原…

2026/10/11 6:05:00 阅读更多 →
08-软件项目管理

08-软件项目管理

第 8 章 软件项目管理 学习目标 掌握软件成本估算的常用方法(专家判断、功能点、COCOMO、类比)理解进度计划(CPM/关键路径)与缓冲管理掌握风险识别、量化与应对理解敏捷与传统项目在计划与度量上的差异熟悉团队组织与沟通管理要点掌握项目沟通与期望管理8.0 项目管理与过程的关…

2026/10/11 6:05:00 阅读更多 →

最新新闻

Qt窗体背景颜色设置指南:QPalette原理与实战

Qt窗体背景颜色设置指南:QPalette原理与实战

如果你稍微搜过 Qt 窗体背景颜色的设置方法,大概率会看到两种方案:一种是setStyleSheet("background-color: ...;"),另一种就是本文要讲的QPalette。我个人的建议是:临时改色用 QSS 很爽,但一旦项目里要做多…

2026/10/11 6:49:27 阅读更多 →
AI短剧工业化生产:定妆资产规范、主体定义模板与判废表全流程指南

AI短剧工业化生产:定妆资产规范、主体定义模板与判废表全流程指南

1. 短剧工业化生产的前置认知1.1 为什么“定妆资产”决定了短剧的生死做AI真人短剧,很多人第一反应是去研究哪个模型出图更真、哪个工具能一键生成视频。但真正跑过完整项目的人都知道,决定一部短剧能不能顺利做完、能不能保持角色一致性的,从…

2026/10/11 6:49:27 阅读更多 →
软件设计之道:构建可落地的架构决策认知体系

软件设计之道:构建可落地的架构决策认知体系

我干了十几年的软件设计和架构,有一个很深的感触:网上讨论架构的内容,大部分是“术”——这块用个什么模式,那块引入什么框架。但真正拖垮项目的,往往是“道”的层面出了问题。很多人掌握了所有主流的技术栈&#xff0…

2026/10/11 6:49:27 阅读更多 →
TIM定时中断

TIM定时中断

TIM定时器介绍:定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断16 位计数器、预分频器、自动重装寄存器的时基单元,在 72MHz 计数时钟下可以实现最大 59.65s 的定时(多久数一次、数了多少次、数到多久重新开始&am…

2026/10/11 6:49:26 阅读更多 →
06_跨平台UI适配与主题系统深度解析:从Windows到Linux的完美呈现

06_跨平台UI适配与主题系统深度解析:从Windows到Linux的完美呈现

06_跨平台UI适配与主题系统深度解析:从Windows到Linux的完美呈现桌面应用跨平台开发最大的挑战是 UI 一致性。本文基于 GPFX 项目在 Windows 和银河麒麟 Linux 的实战经验,系统讲解主题系统设计、字体适配、图标处理、平台差异封装等核心技术&#xff0c…

2026/10/11 6:49:26 阅读更多 →
Tauri实战:用Rust与Web技术打造轻量跨平台桌面应用

Tauri实战:用Rust与Web技术打造轻量跨平台桌面应用

1. 项目概述:Youwee 到底想解决什么问题第一次看到 Youwee 这个名字时,我还在想它会不会又是一个用 Electron 包了三层壳的“伪原生”应用。等真正把基于 Tauri 的架构跑起来之后,我才意识到这确实是一款值得记录的现代化桌面应用。Youwee 的…

2026/10/11 6:48:26 阅读更多 →

日新闻

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

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

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

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

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

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

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

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

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

2026/10/11 0:00:27 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 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/10 5:23:50 阅读更多 →
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/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练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/10 10:38:42 阅读更多 →