news 2026/10/18 1:06:58

ESP32+RC522零基础跑通RFID刷卡读卡全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32+RC522零基础跑通RFID刷卡读卡全流程

做刷卡门禁这个想法,很多人最初是在公司、学校或者小区门口被那“滴”的一声勾起来的。一张小卡片贴近读卡器,门锁就自动弹开,看起来总带点未来感。可当你真的把ESP32和RC522模块接好,第一次在串口监视器里看到那串十六进制卡号稳定打印出来时,那种“魔法被拆穿”的踏实感,比拆一台新手机还要满足。

这篇文章的目标只有一个:零基础跑通RFID无线射频卡读取流程,让ESP32获得“刷卡”能力。我会从方案选型、硬件接线讲起,把RFID的工作原理掰开说清楚,再带你把Arduino IDE里从环境配置到代码烧录全部实现。全程不需要任何电子学基础,你需要的就是一块ESP32开发板、一个RC522读卡模块(套装一般附带卡片和钥匙扣)和一台能连USB的电脑。文末还整理了我实测踩过的坑,以及从“能读卡”到“做成门禁雏形”的扩展思路。

适合谁看?一种是手里有ESP32、却一直不知道能做什么小项目的新手;另一种是对门禁、考勤这类应用感兴趣,想从硬件侧搞懂背后原理的人。这篇不会只丢给你一份代码,而是让你明白每一步为什么要这么接、为什么要这么写。

1. 为什么是ESP32加RC522:我替你做过的方案权衡

1.1 主控选型:ESP32赢在“什么都带”

做刷卡项目,主控的选择其实不止一种。常见的有Arduino Uno、ESP8266、ESP32,再往上有树莓派Pico等。如果你只是验证“读一张卡”,它们都能做到。但ESP32赢在三件事:自带Wi-Fi和蓝牙,意味着刷卡记录可以联网上传或者用手机控制;硬件SPI接口好用,接RC522模块不用自己模拟时序;开发环境成熟,Arduino和MicroPython都能跑,资料多到几乎踩不到没人走过的路。

另一个现实因素是价格。ESP32开发板现在的价格已经压得很低,在主流电商平台基本和Arduino入门板持平,配置却高出一截,双核240MHz、520KB SRAM、4MB Flash,这个规格在物联网领域完全够用。零基础直接选ESP32,等于以后做任何联网项目都不用再换主控。

我在实际项目里的体会是,如果未来想给这个刷卡器加上“远程开门”“微信通知”这类功能,ESP32的Wi-Fi模块能把扩展成本降到零。入门阶段就把主控选对,后面扩展会舒服很多。

1.2 读卡方案:为什么是RC522而不是PN532

市场上最常见的HF高频读卡模块是RC522和PN532。RC522对应的芯片是NXP的MFRC522,工作在13.56MHz,支持读取MIFARE系列IC卡,也就是绝大多数门禁卡、校园卡用的那种类型。它最大的优点是便宜,模块价格在电商平台大概只有一顿早饭的钱,而且读取距离虽然只有几厘米,但门禁刷卡本来就是贴近式操作,不存在“识别距离焦虑”。

PN532的优势是同时支持NFC卡模拟、读写NTAG系列标签,还能跟手机做点对点通信。但它的模块价格大概是RC522的数倍,而且驱动复杂度明显更高。对零基础来说,PN532的学习曲线偏陡,仅仅是“让ESP32能刷卡”这个目标,RC522是成本最低、资料最全的选择。

这个选择背后是一个很朴素的原则:先跑通,再考虑扩展。RC522能把“刷卡”这件事用最小的代价做扎实,等你理解了RFID这套数据交换方式,以后换更高级的模块也是顺理成章的事。

1.3 整体框架:读卡、识别、反馈三件事

整个项目可以拆成三个层次:最底层是RFID模块与卡片的物理射频通信;中间是ESP32通过SPI总线从RC522模块读取卡号;最上层是业务逻辑,判断卡号是否在允许名单里,以及用蜂鸣器、LED、继电器做出对应反馈。

这个分层意识在新手阶段特别重要。很多初学者一上来就想把“门禁系统”整个做出来,结果卡在接线或者一个库版本问题上就放弃了。正确做法是先把“读卡号”跑通,再把“名单判断”加上,最后才轮到“开门动作”。每一层互相独立,调试任何一层都不需要推翻前面所有工作。

1.4 安全提醒:RFID不是保险柜

必须提前说一句:13.56MHz的MIFARE Classic卡片,其加密算法是上世纪九十年代的产物,存在公开的安全漏洞,卡片UID可以被复制。如果只是学习练手,或者做一个宿舍里的模拟门禁,问题不大;但如果是严肃的生产环境门禁,需要选用带更强加密逻辑的NFC卡片,或者采用手机NFC加服务器验证的流程。这个项目虽然是零基础入门,但安全意识从一开始就建立起来,比以后返工划算得多。

2. 硬件原理与接线实操:先搞明白“刷卡”是怎么发生的

2.1 RFID原理的大白话版本

RFID,全称Radio Frequency Identification,翻译过来是射频识别。工作过程很像一次“无线点名”:

读卡器,也就是RC522模块,不断向外发射13.56MHz的射频载波信号。卡片靠近时,内部的天线线圈通过电磁感应获得能量,这和无源线圈给芯片供电的原理一样。卡片被“唤醒”后,芯片通过改变天线负载的方式,把自己的ID数据调制到射频信号上反射回读卡器。于是读卡器就拿到了卡片的UID。

你可以把卡片想象成一个没有电池的感应门牌:读卡器用类似“无线充电”的方式给它供一下电,它就把写在自己芯片里的身份信息报出来。整个读卡过程通常只有几毫秒,人根本感觉不到延迟。

2.2 元件清单与购买建议

这里列一个最小清单:

  • ESP32开发板,任意带USB串口芯片的款式都可以用
  • RC522 RFID读卡模块,很多卖家会搭配两张IC白卡和一个钥匙扣卡
  • 杜邦线若干,母对母或者公对母都行,也可以把模块直接插在面包板上
  • 可选:有源蜂鸣器、LED、继电器模块、12V电子锁,用于组装完整门禁

购买RC522时最容易踩的坑是天线缩水。正品MFRC522模块的天线线圈通常呈圆形或矩形,走线规整,拿在手里有明显厚度。有些仿制模块天线感应面积小,读卡距离会明显缩短。另一个注意点是确认附赠的卡是13.56MHz的高频IC卡,如果买错成低频的125kHz ID卡,RC522完全不读。

2.3 电路连接实操:一张表搞定

RC522模块是3.3V逻辑的,ESP32的IO口也是3.3V电平,所以两者可以直接互连,不需要像Arduino Uno那样再搞电平转换。使用ESP32默认的VSPI引脚,接线如下:

RC522引脚ESP32引脚说明
SDA / CSGPIO5片选信号,选中RC522
SCKGPIO18SPI时钟线
MOSIGPIO23主设备发送数据
MISOGPIO19从设备回传数据
RSTGPIO22硬件复位
IRQ不接本教程用轮询方式,用不到中断
3.3V3.3V模块供电
GNDGND共地

接线要点是“共地”,也就是ESP32的GND必须接RC522的GND。数字电路里信号的高低电平都是相对GND而言的,共地接错,其他线接得再对也是白搭。我建议接好线之后,用万用表的蜂鸣档快速测一遍引脚之间的通断,尤其排查有没有把某根信号线短接到3.3V。

2.4 关于SPI协议的一点直觉

SPI通信协议,你可以理解成一个很规矩的“布告栏”:MOSI线是主的传令员,MISO线是从的应答员,SCK是大家跺脚的节拍器,CS线决定“我现在在跟谁说话”。SPI总线可以同时挂多个从设备,但同一时刻只能有一个从设备被CS拉低选中。

RC522模块的数据交换就是通过这套机制完成的。好在Arduino生态里有现成的SPI库,我们不需要手动操作底层寄存器。但在排查问题时,知道SCK是时钟线、MOSI和MISO是数据线、CS是片选线,能让你少走很多弯路。

3. 开发环境配置与代码实现:把“刷卡能力”装进ESP32

3.1 Arduino IDE配置ESP32开发环境

第一步,安装Arduino IDE,选2.x版本即可,界面比老版舒服很多。打开“文件 → 首选项 → 附加开发板管理器网址”,填入ESP32官方维护的开发板索引地址。然后在“工具 → 开发板 → 开发板管理器”里搜索esp32并安装。

这一步会下载比较多的文件,如果网络状况不太好的话可能要等一会儿。装完之后,“开发板”菜单里会出现ESP32系列,选择“ESP32 Dev Module”即可。端口选择识别到的USB串口,Windows下是COM口,macOS或Linux下通常是类似/dev/cu.usbserial-XXX的名字。如果端口列表里什么都没有,八成是USB转串口芯片的驱动没装好,CP2102或CH340的驱动需要单独安装。

3.2 安装MFRC522库并解决引脚定义问题

在“库管理器”里搜索“MFRC522”,安装最常被引用的那一个版本即可。这个库是目前最通用的,资料量大。唯一的问题是它最早为Arduino Uno设计,默认引脚定义是SS接数字IO10、RST接数字IO9,所以在ESP32上必须在代码开头用宏定义覆盖掉。

3.3 主程序:读取UID并打印到串口

直接上代码。这个程序干的事情很单纯:初始化SPI和RC522模块,然后不停轮询,一旦检测到卡片靠近,读取UID,以十六进制字符串形式打印到串口。

#include <SPI.h> #include <MFRC522.h> #define RST_PIN 22 #define SS_PIN 5 MFRC522 mfrc522(SS_PIN, RST_PIN); void setup() { Serial.begin(115200); SPI.begin(); mfrc522.PCD_Init(); Serial.println(F("RFID Reader Ready, please scan...")); } void loop() { if (!mfrc522.PICC_IsNewCardPresent()) { return; } if (!mfrc522.PICC_ReadCardSerial()) { return; } Serial.print(F("UID: ")); String content = ""; for (byte i = 0; i < mfrc522.uid.size; i++) { if (mfrc522.uid.uidByte[i] < 0x10) { Serial.print(F(" 0")); content += "0"; } else { Serial.print(F(" ")); } content += String(mfrc522.uid.uidByte[i], HEX); Serial.print(mfrc522.uid.uidByte[i], HEX); } Serial.println(); content.toUpperCase(); Serial.print(F("Card Number: ")); Serial.println(content); mfrc522.PICC_HaltA(); mfrc522.PCD_StopCrypto1(); }

这里有几个细节需要解释。首先是十六进制格式化:UID从模块里读出来是一个byte数组,每个字节用大写的HEX打印。如果某个字节小于0x10,也就是十进制的16,它转换成十六进制只有一位,比如7,不补零的话打印出来是“7”,而我要的是“07”。补前导零之后,卡号格式统一为8位十六进制,方便后面做名单比对。

其次是代码里最关键的两行收尾操作,PICC_HaltA()和PCD_StopCrypto1()。它们的作用是让卡片进入HALT状态并停止加密通信,这样下一张卡才能正常进入识别流程。如果少了这两行,卡片靠近一次之后,后续再贴上去就经常没反应了。

编译烧录比较简单:在工具菜单里选好开发板“ESP32 Dev Module”和对应端口,然后点击上传。烧录完成后打开串口监视器,波特率设置为115200,把卡片贴近模块天线,就能看到类似“UID: 3A 1B 4C 2D”的输出。

注意串口监视器的波特率必须和程序里Serial.begin(115200)保持一致,不一致就会出现一段段乱码。

3.4 添加“认卡不认人”的名单校验逻辑

读卡号只是第一步,真正让项目有实际意义的是名单校验。把已知的授权卡号写进一个字符串数组,每次刷卡后循环比对即可。我提供一个示例逻辑:串口打印“允许通过”或“拒绝”,同时用板载LED做出区分。

#include <SPI.h> #include <MFRC522.h> #define RST_PIN 22 #define SS_PIN 5 MFRC522 mfrc522(SS_PIN, RST_PIN); String authorizedCards[] = {"A1B2C3D4", "11AA22BB"}; int authorizedCount = 2; bool checkUID(String uid) { for (int i = 0; i < authorizedCount; i++) { if (uid == authorizedCards[i]) { return true; } } return false; } void setup() { Serial.begin(115200); SPI.begin(); mfrc522.PCD_Init(); Serial.println(F("Authorized card check ready")); } void loop() { if (!mfrc522.PICC_IsNewCardPresent()) { return; } if (!mfrc522.PICC_ReadCardSerial()) { return; } String uid = ""; for (byte i = 0; i < mfrc522.uid.size; i++) { if (mfrc522.uid.uidByte[i] < 0x10) { uid += "0"; } uid += String(mfrc522.uid.uidByte[i], HEX); } uid.toUpperCase(); if (checkUID(uid)) { Serial.println(F("ALLOWED")); } else { Serial.println(F("DENIED")); } mfrc522.PICC_HaltA(); mfrc522.PCD_StopCrypto1(); }

名单判断的原理非常简单,就是把字符串逐一比对。但这里有一个经验之谈:授权卡号不要硬编码在程序里,除非你只是做演示用。因为每烧录一次程序就要重新编译,哪怕只是加一张卡。更合理的做法是把这个名单放到EEPROM或SD卡里,或者通过Wi-Fi从服务器拉取。不过那就是后话了,零基础先写死,能跑通最重要。

4. 让刷卡真正“能用”:反馈、联动与硬件扩展

4.1 给刷卡一个明确的“滴”

门禁刷卡如果没有声音反馈,会非常别扭。那种“滴”的一声,其实是人判断系统正常工作的信任标志。给ESP32接一个有源蜂鸣器,正极接GPIO26,负极接GND,程序里在通过名单校验时让引脚输出高电平,延时200毫秒再拉低,就能听到一声干脆的“滴”。

这里要注意,有源蜂鸣器自带震荡源,给直流高电平就会响。无源蜂鸣器需要给PWM方波才发声,用起来麻烦一些。新手建议直接买有源蜂鸣器模块,省事。另外有些蜂鸣器模块是低电平有效,也就是输入低电平时响,要看模块丝印判断,接反极性就怎么都听不到声音。

4.2 LED指示灯与错误提示

LED正极通过220欧姆电阻接GPIO27,负极接GND。配合蜂鸣器可以做一个简单的状态提示组合:刷卡通过时“滴一声加绿灯亮两秒”,刷卡被拒绝时“滴滴滴三声加红灯闪烁三次”。

蜂鸣器和LED的组合看似简单,但它传递的信号很明确:用户刷卡之后,系统必须在几百毫秒内给出反馈,否则人的体验就会变得“心里没底”。这里建议不要用delay()写死全部流程,因为delay()会阻塞整个程序,刷卡后的连续几秒内模块无法响应下一张卡。更好的方案是用millis()做非阻塞延时,不过零基础阶段用delay()理解逻辑没有大问题,等后续要加OLED屏幕显示时再改造。

4.3 继电器控制电子锁:新手先模拟,进阶再上强电

把继电器模块的输入信号引脚接到GPIO14,COM端接12V电子锁的电源正极,NO端接锁的电源负极,就可以让ESP32用3.3V逻辑控制12V电路的通断。

但说实话,这个环节我不建议新手一开始就接真实锁具。原因有两条:一是电磁锁或电插锁的电流不小,ESP32的3.3V稳压器带不动,必须用外部12V电源独立供电;二是继电器控制的是比逻辑电路高得多的电压,接线一旦出错,轻则烧模块,重则出安全事故。最稳妥的入门路线是先用一个LED模拟“开门”——刷卡通过时LED亮两秒然后熄灭,代表门锁打开又自动落锁。等代码逻辑彻底稳定了,再考虑上真实继电器的连接。

4.4 从延时到状态机

实际门禁里不能只想“判断卡号是否在白名单”这一件事。还要考虑多次刷卡重复触发、开锁期间不重复动作、刷卡失败后的冷却时间等问题。这些在软件上就是最简单的状态机:空闲状态、开锁状态、冷落状态。

比如开锁成功后,设置一个2秒的锁定时长。在这2秒内无论刷多少次卡都不再触发。用一个变量记录上次开锁时间,每次刷卡时判断当前时间与上次开锁时间的差是否超过2秒。这就是从“演示代码”走向“可用设备”的一个分水岭,虽然代码量不大,但思考方式已经从线性流程变成了事件驱动的状态管理。

5. 常见问题与排查心得:实测踩坑实录

先把典型问题整理成一张速查表,后面再展开聊:

问题现象可能原因解决办法
模块指示灯不亮电源没接好测3.3V和GND是否正常
读不到卡接线错误或卡片类型不对核对SPI四线,确认卡是13.56MHz IC卡
读卡距离特别短天线线圈损坏或附近有金属换模块,天线区域悬空
串口显示乱码波特率不一致统一为115200
初始化报错RST引脚没接对检查GPIO22接线
卡片只能读一次缺少收尾操作补上PICC_HaltA和PCD_StopCrypto1
蜂鸣器不响有源无源搞混或极性接反换有源蜂鸣器模块,核对正负极
继电器不动作触发电平不对确认模块是高电平还是低电平触发

5.1 读不到卡,先查电和线

RC522模块上电后,模块上的指示灯通常会被点亮。如果灯不亮,问题集中在电源上,先用万用表量3.3V和GND有没有电压。灯亮了但读不到卡,优先怀疑SPI接线。这里有个小技巧:把卡片贴在模块天线正面,位置要尽量对准线圈中心区域,卡片平面和天线平面保持平行,不要斜着贴,更不要把卡片贴到模块背面,因为天线只在正面PCB那一侧。

5.2 初始化失败,查复位引脚和SPI冲突

MFRC522库输出类似“Communication with the MFRC522 might be the problem”的提示,说明SPI通信没有建立。第一步检查RST引脚是否确实接到了GPIO22,第二步看看GPIO5、18、19、23这几个引脚有没有被其他器件占掉。ESP32的引脚功能复用比较多,如果你同时接了其他SPI设备,先把它们断开再测RC522。

5.3 卡号不稳定,可能是天线附近有金属

RC522的天线线圈对周围金属非常敏感。模块附近如果有金属外壳、大块铜片或者拧紧的螺丝,都会明显缩短读取距离。实测下来,把模块放在金属桌面上测试,读卡距离能比悬空状态缩一半以上。如果打算把模块嵌入盒子内部,外壳要在天线位置开孔,或者把天线直接贴在非金属材质的外壳窗口内侧。

5.4 供电的隐藏坑

ESP32的3.3V稳压器输出能力有限,当你同时带RC522、蜂鸣器、LED和继电器模块时,总电流可能达到几百毫安。很多莫名其妙的“读卡偶发失败”其实是供电不稳导致的。我遇到过一种诡异现象:不接继电器时读卡一切正常,接上继电器后偶尔出现模块初始化失败,查到最后就是3.3V被拉低。解决方式是给RFID和外设单独接一个3.3V稳压模块或者DC-DC降压模块,ESP32和这些外设共地即可。

5.5 别忽视串口监视器里的隐藏信息

当你调用mfrc522.PICC_GetTypeName(piccType)打印卡类型时,常见的输出是“MIFARE 1K”或“MIFARE 4K”,这表示卡片的存储容量。如果输出“Unknown”,说明模块识别了卡片UID但没有识别出卡型,这通常出现在一些非常规品牌的兼容卡上,不影响读UID,但如果后面要往卡片里写入数据,就要额外小心兼容性问题。

6. 从练手到落地:能刷卡的ESP32还能做什么

6.1 门禁与考勤的本地版本

把读卡成功事件通过OLED屏幕显示出来,就能做成一个小型考勤机。给ESP32加一个实时时钟模块,比如常见的DS3231,每次刷卡时记录时间和卡号,数据可以存到SD卡,或者通过Wi-Fi上报到内网服务器。办公场景下,这个方案的硬件成本可能不到成品考勤机的零头,而且数据完全由自己掌握。

6.2 智能储物柜与会员积分系统

RFID的本质是一个“身份确认”的输入设备。把它接到任何需要“身份不同则行为不同”的场景中都有发挥空间。比如做一个智能储物柜:用户刷卡绑定一个柜号,储物柜的电磁锁自动弹开;再次刷卡时取出物品,系统记录存取时间。这个方案在健身房、餐厅、图书馆都有真实需求。而在会员积分场景里,刷卡识别用户,屏幕显示积分余额,充值或消费后自动更新,相比扫码方案更有“专属感”。

6.3 手机也能当卡用

手机NFC功能允许很多设备被模拟成可读的NFC标签。只要你的手机支持NFC,把手机贴近RC522模块,模块同样能读到一串卡号。不同手机的NFC模拟方式和安全策略差异很大,有的手机在锁屏状态下也能被读取,有的必须解锁并打开NFC开关。这相当于在项目里平白加了一个“手机钥匙”的彩蛋,虽然不能替代所有门禁卡,但作为扩展思路很值得一玩。

6.4 更开放的创意场景

回到项目本身,当你理解了“刷卡”只是一次身份识别之后,它就不再局限于门禁了。比如做一张“客户卡”,小朋友把卡片放到读卡器上,音箱播放一段睡前故事;在工作室里给每台设备绑一张RFID钥匙扣,刷卡开启对应设备电源;在公司给每只宠物猫挂一个RFID钥匙扣,就能自动记录“谁在门口”的打卡数据。ESP32的Wi-Fi能力让这些数据可以轻松上报到小程序或简单后端,给个人项目加上一点“联网产品”的味道。

做这个项目我最大的体会是:初学的人很容易在一开始追求“大而全”,卡片识别、继电器、OLED屏幕、服务器上报全部堆在一起,结果反而连最基础的读卡流程都没跑通。我的建议始终是,第一版只做读卡,串口打印出卡号就算赢了第一局;第二版再加入名单判断和声音反馈;等到这些环节都稳定了,再考虑门锁或者联网。一个项目能不能做成,很多时候不是难在技术点,而是难在控制复杂度。RFID刷卡这个入门项目最大的好处就是它足够简单,简单到能让你完整地体验到从“听别人说”到“自己会”的整个过程。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 1:06:47

Unity Shader Graph自定义贴花:深度重建与盒式投影实战

1. 为什么贴花值得用 Shader Graph 自己做在一款射击游戏里&#xff0c;子弹打到墙上要留弹孔&#xff1b;赛车游戏里&#xff0c;轮胎需要划出刹车痕&#xff1b;开放世界项目里&#xff0c;地面涂鸦、井盖标识、角色脚下的血迹&#xff0c;几乎全都是“事后贴上去”的。如果这…

作者头像 李华
网站建设 2026/10/18 1:06:39

STM32纯软件仿真:基于QEMU+CMSIS-SVD的嵌入式开发新范式

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

作者头像 李华
网站建设 2026/10/18 1:06:38

嵌入式项目如何避免重复造轮子:分层抽象与接口驱动设计

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

作者头像 李华
网站建设 2026/10/18 1:04:13

飞控H743接口分配实战:UART/SPI/I2C/CAN引脚复用与调试要点

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

作者头像 李华
网站建设 2026/10/18 1:04:11

国产MCU破亿出货:从选型到量产的实战避坑指南

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

作者头像 李华
网站建设 2026/10/18 1:04:11

FOC驱动板NTC测温改造:从选型到软件保护完整指南

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

作者头像 李华