文字公告横向循环滚动这个需求几乎每个做后台系统或者门户页面的前端都躲不开。产品经理的原话通常是“就一行字让它自己慢慢滚鼠标放上去停一下”听起来像五分钟的活真动手写的时候才发现坑一个接一个滚到末尾突然跳回开头、滚动速度忽快忽慢、窗口一缩放就错位、公告文字长短不一时容器宽度算不准。我在好几个管理后台和数据看板项目里都实现过这个组件从最早用定时器改margin-left的土办法到后来用requestAnimationFrame配合双份内容做无缝循环中间踩过的坑足够写一篇完整的复盘。这篇内容面向的是已经会用 Vue 做基础组件、但在这个具体场景上反复调试不顺畅的开发者。我会把“无缝循环”这件事拆开讲清楚为什么简单的 CSS 动画在文字长度动态变化时会失效、双份内容拼接的临界点怎么算、requestAnimationFrame和 CSStransform各自适合什么情况、鼠标悬停暂停的正确实现姿势以及那些只有真跑起来才会暴露的边界问题。所有代码基于 Vue 3 组合式 API 编写Vue 2 的读者把ref和onMounted换成对应的选项式写法即可逻辑完全一致。1. 先想清楚“循环滚动”到底在循环什么很多人一上来就写animation: scroll 10s linear infinite写完发现文字滚完一遍之后画面会闪一下或者干脆整段文字消失再从头出现。问题不在动画本身而在于没有想明白循环的边界在哪里。1.1 单份内容滚动的死结假设容器宽度 600px公告文字实际宽度 400px。文字比容器短根本不需要滚动直接居中显示就行。但如果公告文字宽度是 1200px容器只能显示 600px那么文字从右往左移动当它完全移出左边界时屏幕上就空了此时必须瞬间把文字拉回初始位置。这个“瞬间拉回”就是肉眼可见的跳变。有人会说那我让动画从translateX(0)走到translateX(-100%)-100%是相对文字自身宽度算的文字刚好完全移出然后infinite让它重新开始。逻辑上没错但视觉上就是一次硬跳因为观众看到的是“文字走光了然后新文字从右边冒出来”中间有一段空白期。公告栏这种需要持续有信息展示的场景空白期是不能接受的。提示判断要不要滚动不能只看文字长度还要考虑容器是否有内边距、是否有前置图标比如喇叭图标。实际可滚动距离 文字实际宽度 图标宽度 间距 - 容器可视宽度这个值小于等于 0 时就应该关闭滚动否则会出现“文字明明放得下却还在动”的怪异现象。1.2 双份内容拼接才是无缝的本质真正无缝的做法只有一个把同一段公告内容渲染两份首尾相接排成一行。第一份滚出去的同时第二份正好补上来当第二份滚到第一份的初始位置时动画重置因为此时画面和初始状态完全一致所以重置是隐形的。这里的关键参数是位移距离。设单份内容含间距宽度为W那么动画的位移终点就是translateX(-W)而不是-100%。因为容器里现在有两份内容总宽度是2W-100%会移动2W那就滚过头了。这个细节是绝大多数实现出错的根源我见过不少代码写着translateX(-100%)却配了两份内容结果滚动距离翻倍速度看起来莫名其妙地快。用表格把这个关系理清楚方案容器内内容位移终点视觉效果单份 -100%1 份文字自身宽度滚完有空白硬跳双份 -100%2 份两份总宽度滚过头速度异常双份 -W2 份单份宽度无缝衔接正确所以核心公式就一句位移距离等于单份内容宽度动画在这个距离上线性循环。剩下的所有工作都是围绕“怎么准确拿到这个 W”以及“怎么让动画稳定执行”展开的。1.3 为什么不用纯 CSS 动画一把梭纯 CSS 方案keyframesanimation的优点是不占主线程、性能好对于内容固定不变的公告栏完全够用。但它的短板也很明显动画时长是写死的而公告文字长度是后端下发的今天三条短公告、明天一条长公告如果时长固定长公告滚得飞快、短公告慢得像蜗牛。要动态改时长就得在运行时计算W并写入style这本身又回到了 JS 计算的路上。更麻烦的是暂停恢复。CSS 的animation-play-state: paused确实能暂停但恢复时是从暂停处继续这个没问题可一旦你在暂停期间修改了内容或者窗口尺寸变了动画的进度和实际布局就对不上了只能整个重启动画视觉上会跳一下。所以我的选择是内容静态、追求极致性能的场景用 CSS 动画内容动态、需要精确控制暂停和重置的场景用requestAnimationFrame手动驱动。下面两套方案都会给出来你按项目实际情况挑。2. 用 requestAnimationFrame 手写一个可控的滚动引擎先讲手动驱动方案因为它把每一帧的逻辑都摊开在你面前理解了它CSS 方案的那些参数你自然就懂了。2.1 组件骨架与双份渲染模板部分很直接外层容器负责裁剪溢出内层轨道放两份相同内容template div classnotice-bar refcontainerRef mouseenterpause mouseleaveresume div classnotice-track reftrackRef :styletrackStyle div classnotice-item refitemRef span classnotice-icon公告/span span classnotice-text{{ text }}/span /div div classnotice-item aria-hiddentrue span classnotice-icon公告/span span classnotice-text{{ text }}/span /div /div /div /template几个设计点值得说明。第二份内容加了aria-hiddentrue因为对屏幕阅读器来说重复朗读两遍公告是干扰这个属性让它只读第一份。两份内容都带图标是为了保证拼接处的视觉连续性——如果只有第一份带图标第二份开头没有滚动到衔接点时图标会突然消失一下很突兀。样式上容器overflow: hidden轨道用display: flex让两份内容横向排列且不换行white-space: nowrap防止文字内部折行.notice-bar { overflow: hidden; width: 100%; position: relative; } .notice-track { display: flex; will-change: transform; } .notice-item { display: flex; align-items: center; flex-shrink: 0; padding-right: 60px; white-space: nowrap; }flex-shrink: 0是关键不加的话 flex 容器空间不够时会压缩子项宽度导致测量出来的W偏小滚动距离算错。padding-right就是两份内容之间的间距它会被算进单份宽度W里这样衔接处才有呼吸感不会两份文字贴在一起。2.2 测量宽度等 DOM 真正布局完成宽度测量必须在 DOM 渲染并完成布局之后onMounted里直接读offsetWidth有时拿到 0尤其是组件在弹窗、标签页里延迟显示的时候。稳妥的做法是等一帧import { ref, onMounted, onBeforeUnmount, nextTick } from vue const containerRef ref(null) const trackRef ref(null) const itemRef ref(null) const trackStyle ref({}) const text ref(这是一条比较长的系统公告内容用于演示横向循环滚动效果) let itemWidth 0 let offset 0 let rafId null let lastTime 0 let paused false const SPEED 60 // 像素每秒 async function measure() { await nextTick() if (!itemRef.value) return itemWidth itemRef.value.offsetWidth const containerWidth containerRef.value?.offsetWidth || 0 if (itemWidth containerWidth) { // 内容放得下不滚动 stop() trackStyle.value { transform: translateX(0) } return } start() }这里有个容易忽略的判断itemWidth containerWidth时直接不滚。注意itemWidth已经包含了padding-right的间距所以这个比较是偏保守的实际文字可能比容器短一点点但加上间距就超了此时滚动也合理因为两份内容拼接后确实需要移动才能看全。如果你希望更精确可以单独测量文字节点宽度再加间距但对公告栏来说这个精度足够了。2.3 逐帧推进的核心循环requestAnimationFrame的回调会传入一个高精度时间戳用它算时间差才能保证不同刷新率屏幕60Hz、120Hz、144Hz上滚动速度一致function step(timestamp) { if (!lastTime) lastTime timestamp const delta timestamp - lastTime lastTime timestamp if (!paused) { offset (SPEED * delta) / 1000 if (offset itemWidth) { offset - itemWidth } trackStyle.value { transform: translateX(${-offset}px) } } rafId requestAnimationFrame(step) }这段代码是整个组件的灵魂逐行拆解一下。delta是距上一帧的毫秒数SPEED * delta / 1000就是这一帧应该移动的像素数这样速度单位是像素每秒和帧率解耦。当offset超过单份宽度itemWidth时减去itemWidth而不是归零因为此时第二份内容正好滚到了第一份的起始位置画面和offset 0时完全一样减去itemWidth后继续累加视觉上就是无限循环没有任何跳变。注意offset - itemWidth而不是offset 0这个减法保留了超出部分的余量。如果某帧因为卡顿delta特别大offset可能一次超出itemWidth很多用减法能保证不丢失这段位移滚动依然平滑。用取模offset % itemWidth效果相同但减法在数值上更直观。lastTime在暂停时也要更新否则恢复的第一帧delta会是一个巨大的值画面会瞬移一大段。所以暂停逻辑里要把lastTime重置为 0 或者持续更新我选择在step里无论是否暂停都更新lastTime这样恢复时delta就是正常的一帧间隔。2.4 暂停、恢复与清理鼠标悬停暂停是公告栏的标配交互实现上只是改一个标志位循环本身不停这样恢复时无需重新计算时间基准function pause() { paused true } function resume() { paused false } function start() { stop() offset 0 lastTime 0 rafId requestAnimationFrame(step) } function stop() { if (rafId) { cancelAnimationFrame(rafId) rafId null } } onMounted(measure) onBeforeUnmount(stop)onBeforeUnmount里必须cancelAnimationFrame否则组件销毁后循环还在跑持有对已卸载 DOM 的引用既浪费性能又可能报错。这个清理在单页应用里尤其重要路由切换频繁时漏掉清理内存会一点点涨上去。3. 内容变化与窗口缩放动态场景的重新测量静态公告栏跑通只是及格线真实项目里公告内容会轮换、浏览器窗口会被拖动、侧边栏会折叠这些都会让之前测好的itemWidth失效。3.1 监听内容变化重新起跑公告文字来自接口可能在组件挂载后才更新。用watch监听文本变化后重新测量并重置滚动import { watch } from vue watch(text, async () { stop() offset 0 trackStyle.value { transform: translateX(0) } await measure() })这里先stop再measure避免旧循环还在跑的时候新宽度已经生效出现一两帧的错位。重置offset和transform是为了让新内容从起点开始滚否则会从旧内容的进度位置接着滚看起来像跳了一段。如果公告是数组轮播多条公告依次显示逻辑要更复杂一些每条公告单独测量宽度切换时重置。我的建议是每条公告都走一遍“测量-判断-启动”的流程不要试图复用上一条的宽度因为不同公告长度差异可能很大。3.2 ResizeObserver 处理容器尺寸变化窗口缩放用resize事件也能做但它只监听窗口容器自身尺寸变化比如侧边栏折叠导致内容区变宽监听不到。ResizeObserver更精准let resizeObserver null function setupObserver() { if (typeof ResizeObserver undefined) return resizeObserver new ResizeObserver(() { stop() offset 0 measure() }) if (containerRef.value) { resizeObserver.observe(containerRef.value) } }在onMounted里调用setupObserveronBeforeUnmount里resizeObserver?.disconnect()。注意回调里要防抖容器尺寸变化可能连续触发多次每次都重启滚动会造成明显卡顿。简单加个setTimeout延迟 100ms 执行测量即可或者用requestAnimationFrame合并同一帧内的多次回调。提示ResizeObserver回调里直接读offsetWidth可能拿到的是变化前的值因为观察者回调触发时机和布局完成的顺序不保证。稳妥做法是在回调里再套一层requestAnimationFrame等下一帧布局稳定后再测量。3.3 字体加载完成后的二次校准这个坑比较隐蔽如果公告文字用了自定义字体组件挂载时字体可能还没加载完浏览器先用后备字体渲染测出来的宽度和最终字体下的宽度不一样。字体加载完成后文字宽度变了但滚动距离还是旧的衔接处就会出现缝隙或重叠。处理办法是监听document.fonts.readyif (document.fonts document.fonts.ready) { document.fonts.ready.then(() { stop() offset 0 measure() }) }这个 Promise 在所有字体加载完成后 resolve此时重新测量一次宽度就准了。对于字体文件较大、首屏加载慢的项目这一步能避免上线后用户看到的滚动错位问题。4. 纯 CSS 方案的适用边界与参数换算有些项目对性能敏感或者公告内容完全静态用 CSS 动画更省心。但参数必须算对否则前面说的那些问题一个不少。4.1 keyframes 与动态时长的换算CSS 方案同样需要双份内容位移终点是单份宽度。由于 CSS 里拿不到 JS 测的像素值通常用 CSS 变量把宽度传进去.notice-track { display: flex; animation: notice-scroll var(--duration) linear infinite; } keyframes notice-scroll { from { transform: translateX(0); } to { transform: translateX(calc(-1 * var(--item-width))); } }--item-width和--duration由 JS 在测量后写入trackStyle。时长换算公式是duration itemWidth / SPEED比如单份宽度 800px、期望速度 60px/s时长就是 13.33s。这个换算必须做否则不同长度的公告速度不一致。暂停用animation-play-state.notice-bar:hover .notice-track { animation-play-state: paused; }一行 CSS 搞定悬停暂停比 JS 方案简洁。但要注意如果暂停期间内容变了动画进度和新的布局对不上必须通过改key或者移除再添加动画类的方式强制重启动画。4.2 两种方案的取舍对照维度requestAnimationFrameCSS animation性能占主线程但逻辑轻量合成层动画性能更优速度控制像素每秒精确需换算时长间接控制暂停恢复标志位控制灵活animation-play-state简单动态内容重新测量即可需强制重启动画调试难度逻辑透明好排查参数分散在 CSS 和 JS适用场景内容动态、交互复杂内容静态、追求性能我的实际选择是公告条这种组件内容动态是常态用requestAnimationFrame更省心如果是纯展示的跑马灯、且内容写死CSS 方案更合适。两者没有绝对优劣看你的内容更新频率和交互复杂度。5. 那些只有真跑起来才会暴露的边界问题代码写完能滚不代表就完事了。下面这些情况我在不同项目里都遇到过每一个都值得单独处理。5.1 首帧闪烁与初始位移组件刚挂载时如果trackStyle初始为空第一份内容会从容器左边缘开始显示等测量完成、动画启动的瞬间transform突然变成translateX(0)如果此时offset已经有值画面会跳一下。解决办法是初始就把transform: translateX(0)写进trackStyle并且测量完成前不启动动画让内容静止显示测量完再平滑起步。另一个细节是will-change: transform要加在轨道上提前告诉浏览器这个元素会变让它提升为合成层减少重绘。但不要滥用一个页面里will-change元素太多反而增加内存开销公告条这种单个元素用是合适的。5.2 文字含富文本或图片时的宽度陷阱如果公告内容不是纯文本而是带a链接、strong加粗、甚至小图标offsetWidth依然能测到总宽度但要注意内联元素的行高和垂直对齐可能让两份内容高度不一致拼接处出现上下错位。统一给.notice-item设align-items: center和固定line-height能解决大部分情况。更麻烦的是内容里有图片且图片懒加载测量时图片还没加载宽度是占位符的宽度图片加载后宽度变了滚动距离就错了。这种场景要么给图片设固定宽高要么在图片load事件后重新测量。公告栏里放图片本身就不常见如果真有建议限制图片尺寸并预加载。5.3 快速连续悬停导致的计时漂移用户鼠标在公告条上快速移入移出pause和resume被频繁调用。因为我的实现里循环不停、只改标志位所以不会有计时问题。但如果你的实现是在pause时cancelAnimationFrame、resume时重新requestAnimationFrame那么每次恢复lastTime都要重置否则第一帧delta异常大导致瞬移。这也是我选择“循环不停、标志位控制”的原因少一个状态就少一类 bug。5.4 无障碍与减弱动画偏好系统设置里开启“减弱动态效果”的用户不应该看到持续滚动的公告。用媒体查询尊重这个偏好media (prefers-reduced-motion: reduce) { .notice-track { animation: none; transform: none; } }JS 方案里则用window.matchMedia((prefers-reduced-motion: reduce)).matches判断为真时直接不启动滚动让公告静态显示并允许横向滚动查看。这既是体验问题也是无障碍的基本要求很多团队会漏掉。6. 把它封装成可复用组件的几个接口设计最后聊聊组件化。这个滚动逻辑在多个页面复用接口设计得好后面维护省一半力气。6.1 props 与事件的合理划分核心 props 就三个text公告内容、speed滚动速度默认 60、direction滚动方向默认从右往左。speed用像素每秒而不是“几秒滚完”因为后者在内容长度变化时速度会变前者才是符合直觉的物理量。direction支持反向滚动实现上就是把offset的累加改成递减位移取正值即可。事件方面暴露item-click让外部处理公告点击跳转比在组件内部写死跳转逻辑灵活得多。如果公告是多条轮播再加一个change事件抛出当前索引。6.2 暴露方法而非只靠 props有些场景需要外部主动控制比如“点击按钮让公告重新从头滚”。用defineExpose暴露restart方法defineExpose({ restart() { stop() offset 0 lastTime 0 measure() } })外部通过模板引用调用noticeRef.value.restart()。这比通过 prop 传一个递增的key强制重建组件要轻量不会触发完整的挂载卸载流程。6.3 一个容易忽略的细节容器宽度为 0 的情况组件放在v-showfalse的容器里或者放在还没展开的折叠面板里挂载时容器宽度是 0itemWidth containerWidth的判断会误判为“需要滚动”因为 0 宽度下任何内容都超宽启动滚动后容器显示出来宽度变了但滚动参数没更新。处理办法是在measure里加一个判断容器宽度为 0 时直接返回不启动也不停止等ResizeObserver检测到宽度变化后再测量。这个细节在弹窗里的公告条场景特别常见不处理的话弹窗一打开公告就是错位的。我在最近一个数据看板项目里就栽在这上面公告条放在一个默认收起的侧边抽屉里抽屉展开后公告滚动距离明显偏大文字衔接处有大段空白。排查了半天才定位到是挂载时容器宽度为 0 导致的测量错误加上宽度判断后问题消失。这类问题不会在开发环境的常规路径下暴露只有把组件放到真实的布局容器里、走完整的交互流程才会出现所以组件写完后一定要在“延迟显示”的场景里验证一遍。滚动公告这个组件本身不复杂复杂的是各种边界条件和动态场景。把双份内容拼接的位移公式记牢把测量时机和重新测量的触发条件想全剩下的就是按项目实际需求在 JS 驱动和 CSS 驱动之间做选择。我个人的习惯是默认用requestAnimationFrame版本因为它把控制权完全交给你遇到奇怪问题时能逐帧打印offset和delta定位比调试 CSS 动画的合成层行为直观得多。
企业数字化 ERP 产品动态
相关推荐
u-center 配置 u-blox GPS 模块:波特率修改与配置保存避坑指南 简介:这份文档面向使用 u-blox GPS 模块的嵌入式开发者与测试人员,系统讲解 u-center 专业测试软件的常用操作与模块配置方法,帮助读者解决波特率修改、参数保存及与单片机联调等实际问题。资源包共 1 个 docx 文件,约 256KB&… · 2026/11/3 13:14:58
LLM推理加速:从GPU到专用加速器的选型实战 上个月我接到一个挺实际的活儿:公司内部那个基于开源模型搭建的LLM问答服务,高峰期延迟从800毫秒一路飙到两秒多,用户已经到后台骂过一轮了。按老思路,加两张GPU卡最省事,但预算审批卡得很严,领导直接问我&… · 2026/11/3 13:14:58
xlsx与xlsm:宏检测、格式转换与批量处理实践 1. 先把 xlsx 和 xlsm 的差别掰开揉碎标题里这个问题的出现频率,比很多人想象的高得多。我这些年帮人处理过的表格里,十份"打不开""格式不对""宏没了"的文件,有六七份本质上是同一件事:文件后缀和里… · 2026/11/3 13:59:51
Redis 8.0拥抱AI:向量搜索+RAG+语义缓存全解析 最近这阵子,技术圈里最热闹的话题之一,就是 Redis 正式官宣全面拥抱 AI 能力。如果你还停留在“Redis 就是一个缓存数据库”的印象,那今天这篇文章建议认真看完。我从 Redis 8.0 的向量搜索(Vector Set)聊到官方 AI 助… · 2026/11/3 13:59:51
计算机进制转换:原理、方法与应用实践 1. 进制转换的基础概念与计算机应用 在计算机科学和电子工程领域,进制转换是一项基础但至关重要的技能。不同于日常生活中使用的十进制系统,计算机内部主要采用二进制(基数为2)、八进制(基数为8)和十六进制… · 2026/11/3 13:59:51
universal-modder:用大语言模型自动化单机游戏模组修改 我最早接触改模组这件事,是从给一个像素风小游戏换贴图开始的。那时候没有教程,全靠把游戏目录翻个底朝天,用十六进制编辑器一个个文件对结构,改坏一次就重装一次。后来做多了,慢慢总结出一套流程:先备份&a… · 2026/11/3 13:59:51
逆向学习法:从交付物反推技能树,告别低效自学 我先说一个可能很多人都有过的经历:收藏夹里躺着几十个"从入门到精通"的教程,课程买了好几套,电脑里堆着各种学习笔记,但真正让你独立做出一个东西的时候,你还是会打开搜索引擎从"Hello World"开始… · 2026/11/3 13:59:51
IDEA新版UI问题分析与旧版还原指南 1. 项目背景与需求分析JetBrains家的IDEA作为Java开发者最常用的IDE工具,每次UI更新都会引发广泛讨论。2023年推出的新UI设计采用了更扁平化的风格,虽然视觉上更现代,但不少老用户反馈操作效率反而下降。根据我的实际体验和社区调研ÿ… · 2026/11/3 13:58:50
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46