news 2026/10/11 1:11:30

ESP32模块化拼装硬件:用ESP-NOW打造可重构的物联网原型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32模块化拼装硬件:用ESP-NOW打造可重构的物联网原型

做过几次实物项目之后,我最大的感受就是:每次把传感器、屏幕、主控焊到同一块板子上,需求一变就得重新画板子,整堆元器件跟着报废。我们几个开发者后来索性换了个思路——既然每个功能模块都要独立调、独立换,为什么不让它们像马赛克一样自由拼起来?于是就有了这套基于乐鑫 ESP32 生态的 ESP-Mosaico 拼装式硬件模块方案:每一块板子都是独立的"马赛克砖",用磁吸和弹簧触点拼接,靠 ESP-NOW 无线组网,想要什么功能就往上加一块,坏了换一块,升级也换一块。这篇文章会完整拆解这套系统的硬件分工、拼装接口协议、自组网方案,以及我们实测下来的各种坑和绕法。适合正在做快速原型、教学套件、或者想摆脱"一项目一板子"的开发者和创客。

先说结论:ESP-Mosaico 不是某一家厂商的成品开发板,而是一套建立在 ESP32-S3 / ESP32-C3 / ESP32-C6 之上的模块化拼装设计思路。核心思路是把主控、显示、传感器、供电、通信拆成独立的 tile(瓦片/砖块),通过标准化的物理接口拼成完整设备。下面是整个项目的完整拆解。

1. 从"单板打天下"到"马赛克式拼装":ESP-Mosaico 想解决的原型开发顽疾

1.1 传统原型开发的三宗罪

如果你做过三个以上的物联网原型项目,大概率遇到过同样的麻烦。第一个问题是"焊死了改不动"。传感器选型想换,屏幕分辨率想换,都得动 PCB,动完 PCB 还要重新验证电源和信号完整性。第二个问题是"坏了不好查"。一块板子上十几个芯片,工作不正常时很难定位是传感器坏了、接线虚了还是主控方向的逻辑有 bug。第三个问题最烦人——"复用等于零"。上一个项目里精心调好的一块传感器板,因为接口定义和供电电压跟新项目对不上,只能拆零件。

ESP-Mosaico 的出发点就是把这三种问题按"模块"切掉。每块 tile 只干一种活,接口统一,拼装靠磁吸和弹簧针,电气连接不需要焊锡。这样一来,一个项目做完,把各 tile 拆下来,换个主控 tile、加一块显示 tile,就能完全变成另一个设备。

1.2 为什么选 ESP32 系列当底座

这个方案没选树莓派那一类 Linux 板,也没选传统单片机最小系统,原因很实际。一是 ESP32 系列从 C3 到 S3 都有完整 Wi-Fi + BLE 协议栈,几块钱级别的成本就能跑 TCP/IP、HTTPS 和各类 mesh 协议;二是不同型号的引脚布局和外设差异可控,可以做成几乎一样的 LCC 封装外围,只是主芯片和内存不同;三是编译工具链统一用 ESP-IDF 和 PlatformIO,模块间共享代码容易。

我们实际用的组合是:计算密集的工作交给 ESP32-S3(双核 240MHz、内置向量指令、支持 JPEG 解码和 AI 加速)、低功耗传感器采集用 ESP32-C6(支持 802.15.4 和低功耗模式)、网络桥接和简单控制用 ESP32-C3。三者之间用 ESP-NOW 作为默认通信手段,因为它不需要建 Wi-Fi 连接,直接通过 MAC 地址在数据链路层发数据帧,交互时延能压到几十毫秒,非常适合"多个本地模块实时协作"的场景。

1.3 一图看懂 Mosaico 的系统结构

这里直接给一个我们在项目文档里用的系统分层示意,方便你建立整体认知:

应用层: 像素墙 / 环境监测 / 遥控小车 / 教学套件 -------------------------------- 中间层: Mosaico 逻辑地址管理、帧协议、邻居发现 -------------------------------- 通信层: ESP-NOW(默认)+ Wi-Fi(需要连云时切换) -------------------------------- 硬件层: Mailbox 拼装总线(磁吸 + 弹簧针 + ID 电阻) -------------------------------- 物理层: Base Tile / Vision Tile / Display Tile / Sensor Tile / Power Tile / Bridge Tile

每一层都有清晰的接口边界。应用层只要调用"第 x 号 tile 显示 rgb(r,g,b)"这样的 API,完全不需要知道这条指令是走弹簧针、ESP-NOW 还是 TCP 出去的。通信层则统一封装成 Mosaico 自己的帧协议,和底层是 ESP-NOW 还是串口无关。

2. 六种 Tile 的分工与"拼接口"设计:磁吸、ID 电阻与邻居识别

2.1 六种标准 Tile 的职责边界

Mosaico 的模块划分不是随便拍的,我们一起做了很多轮原型之后才定下这六类。每个 tile 都严格遵守"单一职责",不搞多功能复合板:

Tile 类型主芯片核心外设典型任务
Base Tile(主控)ESP32-S3MicroSD、按键、RGB LED跑应用逻辑、网页配置、任务调度
Vision Tile(视觉)ESP32-S3 + OV2640摄像头、PIR图像采集、人形感应、像素采样
Display Tile(显示)ESP32-S3 + SK68125×5 LED 矩阵像素渲染、数字/符号显示
Sensor Tile(感知)ESP32-C6SHT30、BH1750温湿度、光照采集、周期上报
Power Tile(供电)无主控(硬件电路)USB-C、锂电、升降压给整组 tile 供 5V 母线
Bridge Tile(桥接)ESP32-C3以太网 PHY、RS485有线网络接入、走 Modbus 等工业协议

这里最容易被误解的是 Power Tile。它不带主控,也没有无线通信能力,但它是整个拼装组的基础——每个 tile 的电源都从拼装母线取,包括主控 tile 自己。所以系统上电顺序是 Power Tile 先输出 5V,其他 tile 再各自通过板载低压差稳压器降到 3.3V。

2.2 拼装接口的机械与电气定义

拼装接口我们叫它 Mailbox 总线,设计目标就三条:单手能拼、反了插不上、接触足够可靠。

机械上,每块 tile 的四个侧面都内嵌了钕磁铁,磁极按照 N/S 交替排列,保证任意两块 tile 拼在一起时同性相斥、异性相吸,不会出现两块 tile 靠自然磁力贴错方向。板对外露两组弹簧针连接器,一组走电源、一组走信号。

电气定义是六针:VCC(5V)、GND、SDA、SCL、TX、RX。细看会发现,这其实是一个把 I2C(SDA/SCL)和 UART(TX/RX)都预留出来的混合总线。实际运行时,绝大多数通信并不走这些物理引脚,而是走 ESP-NOW 无线帧;物理引脚保留为"窄带低功耗备份通道",比如主控 tile 休眠时,Sensor Tile 可以通过 I2C 直接把温湿度数据塞到共享总线上,让主控不用醒来也能靠中断收到数据。

2.3 邻居发现:用一颗电阻识别"你是谁"

拼上去一块 tile,主控怎么知道旁边来的是什么?靠的是 ID 电阻机制。每块 tile 在 ID 引脚上接不同阻值的电阻,主控 tile 通过 ADC 读取相邻 tile ID 引脚上的分压值来判断类型。

具体数值我们是这样定义的:

ID 电阻阻值:1kΩ / 2.2kΩ / 4.7kΩ / 10kΩ / 22kΩ / 47kΩ 对应 Tile 类型:Base / Vision / Display / Sensor / Power / Bridge

上电后,Base Tile 依次扫描六个方向(上下左右和上下叠放两个方向)的 ID 引脚,结合每块 tile 自己的物理安装方向寄存器,就能重建出一张"谁在哪个位置"的拓扑表。这套机制比用 I2C 地址分配简单得多,也快得多——上电 100ms 内就能完成第一轮邻居识别。

我给后来想复刻的朋友一个建议:ID 电阻尽量选 1% 精度的金属膜电阻,并且每块 tile 的采样点做 10 次 ADC 平均。因为磁吸拼装时,弹簧针的接触电阻不是绝对恒定,劣质电阻加一次性采样很容易误判成另一种 tile。

2.4 逻辑地址分配时机

有了邻居识别还不够,因为 ESP-NOW 通信用的是 MAC 地址,而拼装之后我们需要的是一个"第 3 号显示 tile"这样的逻辑编号。所以 Base Tile 在得到拓扑表之后,会进行一次逻辑地址分配。

分配规则是从 Base Tile 自己开始,地址固定为 0x00,然后按广度优先遍历整张拓扑图,给每个 tile 分配 0x01~0xFE 的逻辑地址。这个地址会写进 tile 的 NVS,之后重启不用重新分配。只有当一个 tile 被挪到另一组设备上,MAC 地址和逻辑地址的绑定关系变化时,才会触发重新分配。

3. ESP-NOW 自组织网络:自动编号、心跳保活与多跳中转

3.1 为什么 ESP-NOW 比 Wi-Fi 直连更省心

局域网设备通信最常见的方案是 TCP/UDP 走 Wi-Fi,但在这个场景里有几个问题绕不开。第一,所有设备要加入同一个 Wi-Fi AP,AP 不在或者漫游切换时,模块之间就断了;第二,TCP/IP 协议栈的时延调度不适合高密度的短帧交互,尤其几十个 tile 同时上报数据时,基站侧容易被打爆;第三,配置 Wi-Fi 密码本身就要多做一套配网交互。

ESP-NOW 相当于在 Wi-Fi 物理层之上,提供了一种不需要连接 AP 的短帧广播/单播能力。两片 ESP32 只要互相知道 MAC 地址,就能直接收发数据,不需要路由器,不需要密码。我们把协议栈封装成 Mosaico Net 层之后,上层代码只需要调用:

mosaico_send(logical_addr, type, payload, len);

底层自动查路由表,优先走单播,找不到单播路径就多跳转发。

3.2 帧协议设计:只有八种类型

太复杂的帧协议在嵌入式设备上是灾难。我们把帧类型压缩到八种,涵盖了拼装场景几乎全部需求:

Type 值帧类型用途
0x01ADDR_ASSIGN地址分配回复
0x02NEIGHBOR_LIST上报邻居拓扑
0x03SENSOR_PUSH传感器数据主动上报
0x04DISPLAY_DRAW像素绘制指令
0x05OTA_PUSH固件升级分片
0x06HEARTBEAT心跳保活
0x07LOCK_STEP多显示 tile 同步信号
0x08DIAG_REPORT诊断信息

每条帧的包头固定六字节:0xA5 魔数、源地址、目标地址、帧类型、序列号、CRC8 校验。有效载荷最长 240 字节。设计成固定头的原因是为了嵌入式解析简单,不搞动态变长解析那一套。

3.3 心跳、拓扑变化与"掉线踢出"

任何无线系统都要面对"设备突然消失"的问题。我们的方案是每块 tile 每 3 秒发一次 HEARTBEAT,Base Tile 在 10 秒内没收到某 tile 的心跳,就标记为离线,并把对应逻辑地址释放。重新收到心跳后,再把它重新绑定到原地址。

这里有一个很容易被人忽略的细节:心跳需要加随机抖动。如果所有 tile 都固定每 3 秒同一时刻发心跳,ESP-NOW 在 2.4G 频段的争用会导致部分帧碰撞,然后就会出现"某块 tile 周期性离线"的诡异现象。我们的做法是每块 tile 的心跳周期取 3 秒上下浮动 0~400ms 的随机值,用 MAC 末字节当随机源。改完之后离线误报率降到接近零。

3.4 多跳转发机制

拼装规模一旦超过 8 块 tile,单靠 Base Tile 直接和所有 tile 通信就有点吃力了,距离远的、中间隔着金属壳体也会掉包。我们给 ESP-NOW 加了一个轻量级多跳转发机制,叫 Mosaic Relay。

转发规则只有三条:

  1. 每帧头里带一个 HOP 计数器,初始 0,最大允许 3。
  2. 任意 tile 收到一帧,如果目标地址不是自己、HOP 没超限,就把 HOP+1 后广播出去。
  3. 每个 tile 维护一个最近收到过的帧序列号缓存,重复帧直接丢弃,避免广播风暴。

这套机制看起来非常原始,但实际效果稳定。项目里 12 块 tile 摆成两排,最远端到 Base Tile 隔了 5 块板,用 1 级中转就能达到 99% 以上投递率。要是做简单点,也可以依赖 ESP-MESH,但 MESH 建立全网络拓扑的开销和收敛时间在小规模场景反而更慢。

4. 复现一个完整案例:10×20 马赛克像素照片墙的无线上色流程

4.1 需求拆解与硬件拼法

空讲协议没意思,拿我们做完的一个案例来说——10×20 马赛克像素照片墙。这面墙可以把手机上传的照片变成 200 个 LED 像素组成的马赛克画面,还能叠加时钟和温度。需求拆解下来就三条:

  1. 显示端需要 200 个 LED 像素点,单个显示 tile 是 5×5,所以需要 8 块 Display Tile;
  2. 照片上传和图像处理需要算力,放 Base Tile 上做;
  3. 需要看时间、测环境温度,各加一块 Sensor Tile。

于是硬件拼装是:1 块 Base Tile + 1 块 Power Tile + 1 块 Sensor Tile + 8 块 Display Tile。排布上,Base Tile 放在最左端,Display Tiles 排列成 2 行 4 列,Sensor Tile 放在角落。整个拼装过程不需要任何螺丝和焊锡,磁吸扣上就完成供电和通信连接。

4.2 主控端的图像处理流程

Base Tile 上跑的是一段基于 ESP-IDF 的 C++ 代码。它内部开了一个 HTTP Server,上传 JPEG 照片后,先把图解码为 RGB565 位图,然后压缩到 200 像素宽。因为每个 LED 只能显示一个颜色,压缩后的每个像素直接作为对应 LED 的颜色值。

选择颜色时,我们没有直接用 RGB888,而是通过一个查表法转成 SK6812 的 GRB 格式,再对每个像素做了 gamma 校正,否则低亮度区域会偏色:

uint32_t rgb888_to_grb(uint8_t r, uint8_t g, uint8_t b) { // gamma 2.2 校正 r = gamma_table[r]; g = gamma_table[g]; b = gamma_table[b]; return ((uint32_t)g << 16) | ((uint32_t)r << 8) | b; }

4.3 像素分发与 LED 渲染

200 个像素按每块 Display Tile 负责 25 个像素切分。Base Tile 组帧时,把 25 个 GRB 值(每个 3 字节,共 75 字节)塞进一帧 DISPLAY_DRAW,发送到对应的逻辑地址。Display Tile 收到后,直接把这些数据写进自己的 LED 驱动缓冲区。

这里要提一个关键设计:每块 Display Tile 内部维护了一个全局坐标偏移量。比如第二行第一块 tile,全局 x 偏移是 0、y 偏移是 5。所以 Base Tile 下发的像素数据都是全局坐标,Display Tile 自己换算成本地 5×5 矩阵的下标。

LED 驱动我们用 I2S DMA 的方式在底层“推”数据,不会占用 ESP32-S3 的 CPU 核。每块 Display Tile 的 25 颗 SK6812 级联成一条链,I2S 以 800kHz 的速率连续输出,刷新率能做到 60fps 以上。

4.4 给读者复现的简化版代码路径

想自己搭一个简化版,不用一上来就照搬整个 Mosaico 框架。最小可跑的通路是这样的:

  1. 准备 1 块 ESP32-S3 DevKit 当 Base,1 块 PICO 板当 Display,两者都刷同一个 ESP-NOW 初始化代码;
  2. Base 端通过 UART 排针接一个简易按键,按下时发送一帧DISPLAY_DRAW,payload 里写死 5 个像素的颜色;
  3. Display 端收到后点亮自己的 5×5 LED 矩阵对应的 5 颗灯;
  4. 跑通了,再逐步加入地址分配、邻居发现和 JPEG 解码。
// 伪代码:Base 端发送整帧 mosaico_frame_t frame = { .magic = 0xA5, .src = 0x00, .dst = 0x01, .type = 0x04, // DISPLAY_DRAW .seq = seq_no++, .payload = { pixel_data, 75 }, .crc = calc_crc8(frame) }; esp_now_send(peer_addr, (uint8_t *)&frame, sizeof(frame));

这样一条链路,大概一个周末就能调通。

5. 我在这套件上踩过的大坑:供电、触点、ADC 漂移与 OTA 升级

5.1 第一个坑:5×5 LED 的"猝死式"供电崩溃

马赛克像素墙刚拼出来那会儿,我们犯了一个教科书级的错误——用 USB 口的 5V 直接给 8 块 Display Tile 供电。每颗 SK6812 全白光时电流约 60mA,25 颗就是 1.5A,8 块 tile 同时亮白就是 12A。USB 口瞬间就保护了,然后整个系统陷入"反复上电重启"的死循环。

正确的做法是,每块 Display Tile 都隔离自己的功率路径:板载 DC-DC 降压到 4.2V 给 LED 供电,控制逻辑单独用一个 3.3V LDO。总线上只传 5V 功率和 3.3V 逻辑信号,“大树底下好乘凉”。然后我们还在渲染代码里加了全局亮度限制,启动时统计可见 tile 数量,超过 6 块就把最大亮度阈值从 255 降到 180。

一个值得记录的辅助技巧:给每个 Display Tile 的电源输入端并联一颗 470μF 电解电容。LED 从全灭到全亮的瞬间,电源电压会降一下,储能电容能把这段瞬时压降扛过去,避免主控因为 UVLO(欠压锁定)复位。

5.2 第二个坑:弹簧针触点打火导致随机复位

磁吸拼装的弹簧针,电气接触本质上是一个"半可靠连接"。拼装瞬间容易产生微小打火,导致相邻 tile 的 3.3V 逻辑信号出现毛刺,进而引发主控复位。这个问题在桌面上放着不动时不会暴露,但一旦有人碰了一下整面像素墙,就随机复位一次。

后来我们给每个 tile 的信号线上加了三样东西:

  • 串联 10Ω 电阻,限制瞬时尖峰电流;
  • 并联 100nF 滤波电容到 GND,吸收高频毛刺;
  • I2C 的 SDA/SCL 和 UART 的 TX/RX 都加上 10kΩ 上拉,确保悬空状态不被误判为低电平。

改完之后的整组测试,用手反复拆装 200 次,没有出现一次随机复位。

5.3 第三个坑:ESP-NOW 同频干扰与"集体失聪"

多块 tile 同时上电时,会同时向 Base Tile 发送 NEIGHBOR_LIST 和 ADDR_REQUEST。我们最早在上电阶段没有做错峰,导致 8 块 Display Tile 的请求帧在 2.4G 频段上撞在一起,Base Tile 一个都收不到,表现就是"所有 tile 都亮着电源灯,但组网失败"。

排查链路是这个样子的:先看日志,发现 Base 没有输出任何接收记录,排除发送端问题;又怀疑 MAC 地址白名单配错了,挨个查却发现单播都能通。最后用抓包器抓 ESP-NOW 帧,才发现是上电瞬间的同步碰撞。解决方式前面提到过:根据 MAC 末字节做 0~900ms 随机延迟,把 8 块 tile 的启动发送时间错开。

顺带说一句,上电时如果发现 Base Tile 偶尔能识别出 6 块 tile 而不是 8 块,多半也是这个原因。解决后组网成功率在 30 次冷启动测试里达到 100%。

5.4 第四个坑:ADC 误判邻居类型的温度漂移

ID 电阻识别在 25℃ 室温环境里很准,但到了户外 40℃ 的阳光下,ID 引脚采样值会漂移。我们的测试数据显示,10kΩ 电阻在从 25℃ 升到 60℃ 时阻值变化了近 4%,ADC 读数直接跨到了相邻档位的判断区间,导致 Sensor Tile 被误认成 Bridge Tile。

这个问题的根治办法是放弃纯电阻,改成一枚 1-Wire 的 EEPROM 芯片,每次上电直接读 EEPROM 里的类型代码,不再依赖模拟量判断。我们在 Mosaico 2.0 版本已经全面切换掉 ID 电阻方案。如果你坚持用 ADC 方案,至少要把判断阈值做成"中间留 20% 的死区",并且做一次上电时的双次采样取平均。

5.5 第五个坑:OTA 升级把逻辑地址冲掉了

用过 ESP-IDF 的都知道,esp_ota_ops升级时会把固件分区整体覆盖。但我们把逻辑地址和邻居拓扑存在了 NVS,理论上不会被 OTA 覆盖。问题是第一次升级时,我们图省事调用了nvs_flash_erase(),导致所有 tile 的逻辑地址全部丢失,重新分配后整个网络断了一分多钟。

建议所有做这个项目的开发者,从第一天就写下一条铁律:OTA 代码里永远不允许nvs_flash_erase()。升级流程应该是:Base Tile 从网络或本地文件系统拿到固件包,切成 240 字节的 OTA_PUSH 分片,按顺序广播。每片带 CRC 校验和序列号,任意一帧失败就重传三次。目标是升级过程中其他 Sensor Tile 的数据采集尽量不受影响。

5.6 其他值得说的可维护性设计

除了上面的坑,还想再提两个维护层面容易被忽略的点。

第一,所有 tile 的固件编译时都要开启CONFIG_ESP32_PANIC_PRINT_BACKTRACE=y,这样一旦现场崩溃,至少能在串口拿到栈回溯,而不是靠猜。第二,调试排针不要省。每块 tile 保留一个 4 针串口排针,不要嫌占地方。拼装系统一旦出问题,最痛苦的就是没法单独看某一块板子的日志,有了串口排针可以让每块 tile 单独接调试线查问题。

从项目里带走的一点东西

做了这么久 Mosaico,我最大的体会其实不是"无线组网"或"模块化硬件"本身,而是"试错速度"这个东西。以前改一个像素墙的布局,从画 PCB 到焊板子,一到两周就没了;现在把 tile 拆下来重拼,五分钟完事,代码不用改一行。对做教学、做创意原型、做小批量验证的人来说,这种把硬件当软件一样"重构"的体验非常值得试试。你完全可以沿用这套思路,用现成的 ESP32 开发板加杜邦线先模拟一遍,再决定要不要做成标准模块。如果后续想继续深入,可以从低功耗入手:让 Sensor Tile 在没事件时进入深度睡眠,整组设备用一块 18650 电池跑一个月,那会是下一个很有意思的挑战。

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

ARIMA-BP混合模型实现微信舆情热度精准预测

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

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

领域驱动设计落地指南:从战术建模到六边形架构

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

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

编译原理实验报告写作指南:从词法分析到语法分析的完整拆解

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

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

AUTOSAR以太网协议栈仿真套件:在PC上跑通SOME/IP与DoIP

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

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

5G基站硬件安装实战指南:从BBU到AAU的站点验收与避坑要点

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

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

OTC承兑平台系统源码拆解:从部署到二开全流程

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

作者头像 李华