微信小程序圆环进度条实现:Canvas 2D与CSS方案对比及性能优化
1. 从一个圆环说起为什么小程序里的进度环值得单独拿出来做微信小程序里做数据可视化很多人第一反应是引图表库。但真到了项目里你会发现一个简单的圆环进度条用图表库往往是杀鸡用牛刀——包体积上去了定制灵活性反而下来了。我最近在几个小程序项目里反复用到圆环组件从最开始的canvas硬画到后来用纯 CSS 方案再到结合canvas 2d做动画踩了不少坑也攒了一套比较稳的做法。所谓“微信小程序圆环”说白了就是在小程序环境里绘制一个环形进度指示器常见形态包括运动步数环、任务完成度环、健康数据环、加载进度环、评分环等等。它的核心诉求通常有三点能根据数据动态变化、有平滑的动画过渡、样式可定制且性能不拖后腿。适合谁看如果你正在做小程序需要自己实现一个不依赖重型图表库的圆环组件或者你已经在用某个库但发现改不动、卡顿、真机表现和开发者工具不一致那这篇内容应该能帮你省下不少试错时间。我下面会从方案选型、核心原理、完整实现、参数计算、真机避坑几个角度把这件事讲透。所有代码都是可以直接抄进项目里跑的参数怎么算我也会把过程写出来不丢一个“你自己调一下”就完事。2. 方案选型三种主流画法到底怎么选2.1 canvas 方案灵活但要注意层级和性能canvas是小程序里画圆环最“正统”的方式。它给你一块画布你用arc画圆弧用lineWidth控制环的粗细用lineCap控制端点圆角。优点是控制粒度极细渐变色、虚线、多段环、带刻度的环都能画。缺点是它是原生组件层级最高会盖住普通view而且旧版canvas接口在真机上偶发闪烁。小程序现在推荐用canvas type2d也就是 Canvas 2D 接口。它和旧接口最大的区别是旧接口用wx.createCanvasContext是一套自绘的上下文新接口通过SelectorQuery拿到真实的canvas节点再取getContext(2d)和 Web 标准几乎一致。这意味着你在网上找到的 Canvas 2D 教程基本能直接迁移学习成本低很多。// 获取 canvas 2d 上下文的典型写法 const query wx.createSelectorQuery().in(this) query.select(#ringCanvas) .fields({ node: true, size: true }) .exec((res) { const canvas res[0].node const ctx canvas.getContext(2d) const dpr wx.getSystemInfoSync().pixelRatio canvas.width res[0].width * dpr canvas.height res[0].height * dpr ctx.scale(dpr, dpr) // 到这里就可以正常画了 })这里有个关键点必须做 dpr 缩放。小程序的 canvas 节点默认按逻辑像素算如果不乘pixelRatio在高清屏上圆环边缘会有明显锯齿。我见过不少人抱怨“圆环糊”九成是漏了这一步。2.2 纯 CSS 方案轻量但有天花板纯 CSS 画圆环主流做法是用conic-gradient配合遮罩或者用两个半圆旋转拼接。conic-gradient在小程序里的兼容性这几年好了很多基础库较新版本都支持。它的好处是没有原生组件层级问题可以和普通元素自由叠放动画用 CSS transition 就能跑性能在简单场景下很稳。但它的问题也很明显conic-gradient做圆角端点很麻烦做渐变色环、多段不同颜色的环、带间隙的环都比较别扭。而且部分低端安卓机对conic-gradient的渲染质量参差可能出现色带。所以我的经验是如果只是单色、无圆角端点、数据变化不频繁的进度环CSS 方案够用且最省心一旦涉及渐变、圆角、多段直接上 canvas。2.3 混合方案CSS 打底 canvas 点缀还有一种我比较喜欢的折中做法外层容器、文字、背景环用普通view和 CSS 做只有需要精细绘制的那一层用 canvas。这样文字不会被 canvas 盖住布局也好调。比如步数环背景灰环用 CSS 的border或box-shadow做前景彩色进度用 canvas 画中间的数字用view绝对定位。这样既拿到了 canvas 的绘制能力又避免了整块区域都是原生组件导致的交互和层级麻烦。选型这件事没有绝对答案我给一个简单的判断表需求特征推荐方案理由单色、无圆角、静态或低频变化纯 CSS最轻无层级问题渐变、圆角端点、多段环Canvas 2D绘制自由度高需要叠文字和按钮混合方案规避原生组件层级数据高频刷新如实时进度Canvas 2D 节流避免频繁 setData需要截图导出Canvas 2D可直接导出图片3. 核心原理拆解一个圆环到底由什么构成3.1 圆弧的起点、终点和角度换算圆环的本质就是一段圆弧。Canvas 的arc(x, y, radius, startAngle, endAngle, anticlockwise)里角度用的是弧度而且0 弧度在三点钟方向顺时针为正。这和我们平时说的“从十二点开始”不一样所以画进度环时通常要把起点旋转到十二点方向。具体做法是在画之前ctx.rotate(-Math.PI / 2)把整个坐标系逆时针转 90 度这样 0 弧度就对应十二点了。或者你在算角度时直接减去Math.PI / 2。两种都行我习惯用rotate因为后续画刻度、画端点都好算。进度百分比到弧度的换算很直接endAngle startAngle (percent / 100) * Math.PI * 2比如 75% 就是0.75 * 2π 1.5π弧度也就是 270 度。这个换算看着简单但实际写的时候容易在“百分比是 0 到 1 还是 0 到 100”上翻车建议统一用 0 到 1 的小数命名成progress避免歧义。3.2 环宽、半径和画布尺寸的关系这是最容易出问题的地方。Canvas 画线是以路径为中心向两侧各扩lineWidth / 2。也就是说如果你在半径r处画一条宽w的弧它实际占据的半径范围是r - w/2到r w/2。如果r w/2超过了画布的一半圆环就会被裁掉。所以正确的半径计算是radius canvasSize / 2 - lineWidth / 2 - padding其中padding是留给端点圆角和阴影的余量。我一般会留lineWidth / 2作为 padding这样圆角端点也不会被切。举个例子画布 200px环宽 16px那么radius 100 - 8 - 8 84。这个数不是拍脑袋来的是保证最外沿刚好贴边又不裁切。注意如果你用了lineCap: round端点会额外向外凸出lineWidth / 2所以 padding 至少要留够这个量否则圆头会被削平。3.3 动画的驱动方式requestAnimationFrame 还是定时器小程序里做动画requestAnimationFrame在 Canvas 2D 环境下是可用的通过canvas.requestAnimationFrame。它比setInterval好在两点一是跟着渲染帧走不会出现定时器和渲染不同步导致的抖动二是页面隐藏时会自动暂停省电。但要注意requestAnimationFrame的回调里如果每帧都setData那性能会崩。正确做法是动画过程中只操作 canvas 上下文不碰 setData。等动画结束再把最终值同步到 data 里用于显示文字。这个原则我后面会在实操部分再强调一次。缓动函数的选择也影响手感。线性动画看起来机械我一般用easeOutCubiceased 1 - Math.pow(1 - t, 3)t是从 0 到 1 的时间进度。这个函数前期快后期慢收尾自然适合进度环这种“冲上去然后稳住”的场景。4. 完整实操从零写一个可复用的圆环组件4.1 组件结构与初始化我把它做成一个自定义组件对外暴露progress、size、lineWidth、colors等属性。结构上就一个 canvas 加一个居中的文字层。!-- ring.wxml -- view classring-wrap stylewidth:{{size}}px;height:{{size}}px; canvas type2d idringCanvas classring-canvas stylewidth:{{size}}px;height:{{size}}px; /canvas view classring-center slot/slot /view /view/* ring.wxss */ .ring-wrap { position: relative; } .ring-canvas { display: block; } .ring-center { position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; pointer-events: none; }初始化在attached生命周期里做拿到 canvas 节点后存起来避免每次重绘都重新查询。Component({ properties: { progress: { type: Number, value: 0, observer: onProgressChange }, size: { type: Number, value: 200 }, lineWidth: { type: Number, value: 16 }, colors: { type: Array, value: [#4facfe, #00f2fe] }, duration: { type: Number, value: 800 } }, data: {}, lifetimes: { attached() { this.initCanvas() } }, methods: { initCanvas() { const query this.createSelectorQuery() query.select(#ringCanvas) .fields({ node: true, size: true }) .exec((res) { if (!res[0]) return const canvas res[0].node const ctx canvas.getContext(2d) const dpr wx.getSystemInfoSync().pixelRatio canvas.width res[0].width * dpr canvas.height res[0].height * dpr ctx.scale(dpr, dpr) this.canvas canvas this.ctx ctx this.draw(this.data.progress, false) }) } } })4.2 绘制函数背景环 进度环 渐变绘制分三步清空、画背景环、画进度环。渐变用createLinearGradient或createConicGradient后者兼容性一般我一般用线性渐变模拟。draw(progress, animate true) { const ctx this.ctx const size this.data.size const lw this.data.lineWidth const radius size / 2 - lw / 2 - lw / 2 const center size / 2 ctx.clearRect(0, 0, size, size) // 背景环 ctx.beginPath() ctx.arc(center, center, radius, 0, Math.PI * 2) ctx.strokeStyle rgba(0,0,0,0.06) ctx.lineWidth lw ctx.stroke() // 进度环 if (progress 0) return const start -Math.PI / 2 const end start progress * Math.PI * 2 const grad ctx.createLinearGradient(0, 0, size, size) grad.addColorStop(0, this.data.colors[0]) grad.addColorStop(1, this.data.colors[1] || this.data.colors[0]) ctx.beginPath() ctx.arc(center, center, radius, start, end) ctx.strokeStyle grad ctx.lineWidth lw ctx.lineCap round ctx.stroke() }这里radius我减了两次lw/2一次是线宽本身一次是圆角端点的余量。如果你不用圆角端点减一次就够。这个细节决定了圆环会不会被裁务必按自己的lineCap调整。4.3 动画实现逐帧插值不碰 setData动画的核心是记录起始值、目标值、开始时间然后在每一帧里算当前值重绘。animateTo(target) { const from this.currentProgress || 0 const duration this.data.duration const startTime Date.now() const canvas this.canvas const step () { const elapsed Date.now() - startTime let t Math.min(elapsed / duration, 1) // easeOutCubic const eased 1 - Math.pow(1 - t, 3) const current from (target - from) * eased this.currentProgress current this.draw(current, false) if (t 1) { canvas.requestAnimationFrame(step) } else { this.currentProgress target // 动画结束再同步文字 this.setData({ displayProgress: Math.round(target * 100) }) } } canvas.requestAnimationFrame(step) }注意this.currentProgress是挂在组件实例上的普通属性不是 data。这样每帧重绘不会触发 setData性能差别在真机上非常明显。我实测过一个 60 帧的动画用 setData 驱动在中端安卓机上会掉到 30 帧以下改成直接操作 canvas 后稳定 55 帧以上。4.4 参数计算实例给定尺寸反推所有数值假设设计稿要求圆环外径 240px环宽 20px圆角端点居中显示百分比文字。我们来反推画布尺寸取 240px外径即画布尺寸因为圆环贴边环宽lineWidth 20半径radius 240/2 - 20/2 - 20/2 120 - 10 - 10 100验证最外沿100 10 110加上圆角端点再凸10正好120贴边不裁文字区域内圈半径100 - 10 90直径 180px足够放两行文字这套算法你换成任何尺寸都成立核心就是radius size/2 - lineWidth圆角端点情况下。记住这个公式比记代码有用。5. 真机避坑与常见问题排查5.1 圆环模糊、锯齿、边缘发虚这是最高频的问题原因基本就三个没做 dpr 缩放、canvas 的 CSS 尺寸和属性尺寸不一致、或者用了旧版 canvas 接口。排查顺序先确认canvas.width 逻辑宽 * dpr再确认ctx.scale(dpr, dpr)调用了最后确认 CSS 里的宽高和fields拿到的 size 一致。三个都对边缘就是清晰的。还有一个隐蔽情况如果你在wxss里给 canvas 设了border-radius或者overflow: hidden某些机型上会触发额外的合成层导致边缘发虚。圆环本身不需要圆角裁剪去掉这些样式即可。5.2 动画卡顿、掉帧、真机与工具不一致开发者工具是模拟环境性能远好于真机所以工具里流畅不代表真机流畅。掉帧的常见原因动画里 setData、每帧重新创建渐变对象、canvas 尺寸过大。优化手段动画期间零 setData、渐变对象缓存起来复用、canvas 尺寸不要超过实际显示尺寸的 2 倍。我遇到过一个典型案例圆环放在长列表里每个 item 一个 canvas滚动时严重卡顿。后来改成只渲染可视区域的 canvas滚出屏幕就销毁卡顿消失。canvas 是重资源能少用就少用能复用就复用。5.3 进度为 0 或 100 时的显示异常进度 0 时如果直接画arc(start, start)某些实现会画出一个小点或者整圆。正确做法是进度为 0 时直接跳过进度环绘制。进度 100 时endAngle和startAngle相差正好2π圆角端点会首尾重叠看起来像有个小疙瘩。解决办法是 100% 时把lineCap临时改成butt或者把终点角度减一个极小值如0.001弧度让端点错开。5.4 常见问题速查表现象可能原因解决方向圆环被裁切半径算大了没留端点余量半径减lineWidth边缘模糊未做 dpr 缩放乘 pixelRatio 并 scale动画卡顿动画中 setData改为直接操作 canvas文字被盖住canvas 原生层级高文字层用 cover-view 或混合方案进度 100% 有疙瘩圆角端点重叠终点减极小值或改 lineCap低端机色带渐变步进不足增加 colorStop 或改纯色页面隐藏后动画错乱rAF 未暂停监听页面生命周期手动暂停5.5 几个我踩过的坑第一个坑在observer里直接调draw但此时 canvas 可能还没初始化完attached是异步拿节点的。解决方法是加一个this.ready标志初始化完成前把进度存起来初始化后再补画。第二个坑组件复用比如列表里时createSelectorQuery拿到的是当前组件实例的节点但如果你在page里查会拿到第一个匹配的。一定要用this.createSelectorQuery()而不是wx.createSelectorQuery()。第三个坑canvas.requestAnimationFrame在组件销毁后如果还在跑会报错。记得在detached里取消动画用一个this.animating标志控制。6. 进阶玩法与性能优化建议6.1 多段环与刻度环多段环就是把整圈按比例切成若干段每段单独画弧段与段之间留间隙。做法是维护一个累计角度每段从累计起点画到累计终点然后累计角度加上间隙。刻度环则是画一圈短线段用ctx.save/restore配合rotate逐条画或者用极坐标算每条线的起止点。这两种在数据看板类小程序里很常见掌握了基础圆弧后都不难。6.2 用离屏 canvas 预渲染静态部分如果背景环、刻度这些是静态的可以只在初始化时画一次存成图片或者保留在离屏 canvas 上动画时只重绘进度环。小程序支持wx.createOffscreenCanvas用它预渲染能进一步降负载。不过对于单个圆环收益有限列表场景收益才明显。6.3 性能优化的三条硬规矩第一动画期间不 setData这是铁律。第二渐变、路径对象能缓存就缓存别每帧 new。第三canvas 数量能少就少列表里用图片替代或做可视区域渲染。这三条做到圆环在小程序里的性能基本不会成为瓶颈。我个人在实际项目里的体会是圆环这种组件看着简单但真要做好功夫全在细节上——半径差几个像素、dpr 少乘一次、动画里多一次 setData用户不一定说得出哪里不对但就是觉得“不精致”“有点卡”。把这些细节抠到位一个圆环也能成为整个页面最耐看的部分。后续如果要做环形菜单或者环形拖拽选择器这套圆弧计算和动画框架可以直接复用改的只是交互逻辑。

相关新闻

dsoframer.ocx 部署排错:Office 2016 网页白屏问题解决指南

dsoframer.ocx 部署排错:Office 2016 网页白屏问题解决指南

简介:面向WinForm开发者的DSOFramer.ocx控件最新版,用于在应用程序窗体中直接嵌入Office 2016文档,解决低版本控件导致的Word以独立窗口打开、破坏界面统一性的痛点。安装包共14个文件,以ocx主控件、dll互操作程序集、exe示例程序…

2026/10/16 11:34:12 阅读更多 →
数据在内存中的存储:从字节、地址到补码与大小端

数据在内存中的存储:从字节、地址到补码与大小端

1. 从硬件视角看内存:先弄懂地址和字节 1.1 内存就是一排带编号的字节抽屉 先说一个很基础但很多人从来没认真想过的点:内存到底是什么? 你可以把内存想象成一条超长的抽屉柜,柜子里的每个抽屉只能放一个字节,也就是…

2026/10/16 11:33:11 阅读更多 →
ResNet深度解析:从退化问题到残差块实现,掌握深度学习核心架构

ResNet深度解析:从退化问题到残差块实现,掌握深度学习核心架构

1. 从“堆层数”到“残差学习”:ResNet到底在解决什么问题如果你在2015年之前尝试过训练一个超过20层的卷积神经网络,大概率会碰到一个非常反直觉的现象:训练误差不降反升。这不是过拟合——过拟合的表现是训练误差持续下降而验证误差上升&am…

2026/10/16 11:33:11 阅读更多 →

最新新闻

SIMATIC PC Adapter USB A2驱动安装与MPI通信设置全攻略

SIMATIC PC Adapter USB A2驱动安装与MPI通信设置全攻略

简介:SIMATIC PC Adapter USB A2驱动包是西门子专为老款PLC设计的通信适配器驱动程序,面向需要将不具备以太网接口的S7系列PLC接入PC的工程师与维护人员。该驱动通过USB物理连接,配合STEP 7等编程软件,即可完成离线/在线编程、程序…

2026/10/16 12:18:26 阅读更多 →
SQL语句与ADO编程:从查询优化到参数化事务的完整避坑指南

SQL语句与ADO编程:从查询优化到参数化事务的完整避坑指南

我一直觉得,做ADO编程的人如果SQL底子不牢,后面每写一个功能都会觉得哪儿哪儿都不顺。看代码、调连接、改参数,最后发现错的全是SQL语句本身,这种滋味我太熟悉了。这篇“SQL语句与ADO编程学习总结”系列的第一篇,我就专…

2026/10/16 12:18:26 阅读更多 →
MindSpore、ModelArts、昇腾一年实测:训练到部署的避坑指南

MindSpore、ModelArts、昇腾一年实测:训练到部署的避坑指南

大概是去年年初,我第一次认真评估 MindSpore、ModelArts 和昇腾芯片这条技术路线。起因倒不是什么宏大规划,就是一个实际得不能再实际的问题:模型训练完以后要部署到昇腾设备上,团队希望训练侧和推理侧尽量少一些框架切换的摩擦&a…

2026/10/16 12:18:26 阅读更多 →
Vuetify 3 实战指南:从 Vite 搭建到组件应用与性能优化

Vuetify 3 实战指南:从 Vite 搭建到组件应用与性能优化

Vuetify 3 推出也有一段时间了,记得从 Vuetify 2 往上升级那会儿,身边不少同事都卡在安装和配置上。其实这套基于 Vue 3 的 Material Design 组件库,上手路径已经很清晰了,难的是很多教程还停留在旧版 API 的思维里。这篇我把从零…

2026/10/16 12:18:26 阅读更多 →
数字贸易限制指数数据处理与实证建模全流程解析

数字贸易限制指数数据处理与实证建模全流程解析

做贸易政策实证这几年,我越来越确信一件事:数字贸易研究缺的往往不是模型,而是一组干净、可解释、能直接进回归的政策指标。最近我在整理数字贸易限制指数的基础设施与电子交易分项数据(覆盖2014—2024年),…

2026/10/16 12:18:26 阅读更多 →
Agent 别裸奔进生产:先搭架构、可观测、人审三道闸

Agent 别裸奔进生产:先搭架构、可观测、人审三道闸

摘要:Agent 本地跑通,不等于生产可靠。本文给三道闸——架构分层、可观测、人审——把"想"和"做"拆开、给每一步留审计轨迹、把不可逆操作锁在人审之后,并用 ThinkingBox 的静默失败数据说明,为什么不能信 Ag…

2026/10/16 12:17:24 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

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

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 0:14:36 阅读更多 →
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/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练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/15 9:49:14 阅读更多 →