Umbraco Backoffice 拖拽排序实战:基于 UmbSorterController 实现嵌套容器 Sorter 示例解析
CMS后端【免费下载链接】Umbraco-CMSUmbraco is a free and open source .NET content management system helping you deliver delightful digital experiences.项目地址https://gitcode.com/gh_mirrors/um/Umbraco-CMS点击查看免费下载本文以 Umbraco-CMS 仓库中的sorter-with-nested-containers示例包为主体完整讲解如何在 Umbraco Backoffice 中配置并运行UmbSorterController重点剖析“嵌套容器nested containers”场景下的接入方式、数据模型同步机制以及控制器底层基于原生 HTML5 Drag and Drop 的跨容器移动原理。读完后你可以照示例搭建自己的多级拖拽排序界面并理解每个配置项在源码中的作用。示例定位它演示什么示例位于 examples/sorter-with-nested-containers 目录其 README.md 对目标只有一句话This example demonstrates how to set up the Sorter and how it can be used in nested setups.也就是说它演示两件事如何接入 SorterUmbSorterController以及如何在“容器套容器”的嵌套结构中协同工作。示例目录包含 4 个文件各自职责如下文件职责index.ts声明 dashboard manifest把示例注册进 Backofficesorter-dashboard.ts顶层 dashboard 元素持有两组数据并渲染两个排序分组sorter-group.ts可复用的“分组”元素内部实例化UmbSorterController并渲染嵌套分组sorter-item.ts可拖拽的条目元素带操作 slot 与占位样式按照 examples 目录的说明这些示例包既是扩展用法的演示也用于验证各扩展点持续可用。仓库中还有一个平行的示例 sorter-with-two-containers演示两个平级容器间的移动两者 manifest 别名相同example.dashboard.dataset由于一次只会运行一个示例包互不冲突。运行示例包示例由 devops/example-runner/index.js 驱动。在src/Umbraco.Web.UI.Client目录下执行# 交互式选择示例列出 examples/ 下所有子目录支持方向键/输入过滤 npm run example # 或直接指定本示例 npm run example sorter-with-nested-containers # 使用 MSW mock 数据运行对应 runner 的 --mock 参数底层执行 npm run dev:mock npm run example:mock sorter-with-nested-containers从 runner 源码可以看到其工作方式选中示例后设置环境变量VITE_EXAMPLE_PATHexamples/sorter-with-nested-containers然后调用npm run devmock 模式为npm run dev:mock启动 Vite 开发服务器由前端构建流程把该目录作为扩展包加载。脚本还会校验工作目录若找不到examples目录会提示 “Run this from src/Umbraco.Web.UI.Client”。Manifest把 dashboard 注册进 Backofficeindex.ts 声明了一个ManifestDashboardimport type { ManifestDashboard } from umbraco-cms/backoffice/dashboard; export const manifests: ArrayManifestDashboard [ { type: dashboard, name: Example Sorter Dashboard, alias: example.dashboard.dataset, // 懒加载 dashboard 组件 element: () import(./sorter-dashboard.js), weight: 900, meta: { label: Sorter example, pathname: sorter-example, }, }, ];各字段含义alias扩展包全局唯一标识element返回 Web Component 模块的懒加载函数meta.label/meta.pathname决定该 dashboard 在 Backoffice 导航中的显示名称与路由路径本例为sorter-exampleweight排序权重。顶层结构两个分组、可递归嵌套sorter-dashboard.ts 定义了example-sorter-dashboard自定义元素它维护两个ModelEntryType[]数组并渲染两个example-sorter-groupcustomElement(example-sorter-dashboard) export class ExampleSorterDashboard extends UmbElementMixin(LitElement) { groupOneItems: ModelEntryType[] [ { name: Apple, children: [{ name: Juice }, { name: Milk }] }, { name: Banana, children: [] }, { name: Pear }, { name: Pineapple }, { name: Lemon, children: [{ name: Cola }, { name: Pepsi }] }, ]; groupTwoItems: ModelEntryType[] [ { name: DXP }, { name: H5YR }, { name: UUI }, ]; override render() { return html uui-box classuui-text div classouter-wrapper example-sorter-group .value${this.groupOneItems} change${(e: Event) { this.groupOneItems (e.target as ExampleSorterGroup).value; }}/example-sorter-group example-sorter-group .value${this.groupTwoItems} change${(e: Event) { this.groupTwoItems (e.target as ExampleSorterGroup).value; }}/example-sorter-group /div /uui-box ; } }注意两点数据是自引用树形结构——每个条目可以带children这正是“嵌套容器”的数据基础dashboard 只负责“状态提升”把.value属性传给分组元素并在change事件后把分组的新模型读回来赋给自身状态触发 Lit 重渲染。数据模型定义在 sorter-group.ts 中export type ModelEntryType { name: string; children?: ModelEntryType[]; };核心接入ExampleSorterGroup 与 UmbSorterController 配置sorter-group.ts 是示例的关键——一个分组元素内部实例化一个控制器#sorter new UmbSorterControllerModelEntryType, ExampleSorterItem(this, { getUniqueOfElement: (element) { return element.name; }, getUniqueOfModel: (modelEntry) { return modelEntry.name; }, identifier: string-that-identifies-all-example-sorters, itemSelector: example-sorter-item, containerSelector: .sorter-container, onChange: ({ model }) { const oldValue this._value; this._value model; this.requestUpdate(value, oldValue); // Fire an event for the parent to know that the model has changed. this.dispatchEvent(new CustomEvent(change)); }, });各配置项结合 控制器源码 逐一说明配置项示例取值源码中的默认值/作用getUniqueOfElementelement.name必选。由 DOM 元素提取唯一标识返回undefined会取消本次移动getUniqueOfModelmodelEntry.name必选。由模型条目提取唯一标识用于元素 ↔ 模型互查identifierstring-that-identifies-all-example-sorters缺省为Symbol()源码config.identifier ?? Symbol()即每个控制器实例默认互不相通显式共享同一字符串后所有带该 identifier 的 Sorter 之间才允许跨容器拖放——这是本示例嵌套/跨组拖拽能成立的前提itemSelectorexample-sorter-item必选。可拖拽条目元素的查询选择器containerSelector.sorter-container缺省为宿主元素自身从host.shadowRoot中按选择器查找找不到会抛出 “Sorter could not find the container element” 错误ignorerSelector未设置默认a,img,iframe,input,textarea,select,option这些元素及draggablefalse的元素不触发拖拽placeholderClass/placeholderAttr未设置二者都未设置时默认placeholderAttr drag-placeholder即拖拽占位符会被加上该属性条目组件据此降低透明度onChange更新模型并派发change模型发生 move/insert/remove 后的统一回调除本例用到的字段外UmbSorterConfig还支持disabledItemSelector、draggableSelector指定元素内部真正可拖拽的子元素、handleSelector指定唯一可交互的拖拽把手、dataTransferResolver向 DataTransfer 写入额外数据、onStart/onEnd、onDisallowed/onAllowed/onRequestMove允许/拒绝指示、onRequestDrop接收来自其他窗口/标签页的原生拖放、requestExternalRemove/requestExternalInsert跨 Sorter 感知范围的外部移除/插入、resolvePlacement自定义放置位置判定返回true/false决定前后放置、返回null阻止移动以及细粒度的performItemMove/performItemInsert/performItemRemove可返回Promiseboolean拦截具体操作。value属性是模型与控制器之间的桥梁property({ type: Array, attribute: false }) public get value(): ModelEntryType[] { return this._value ?? []; } public set value(value: ModelEntryType[]) { const oldValue this._value; this._value value; this.#sorter.setModel(this._value); // 父级重渲染时把最新模型同步进控制器 this.requestUpdate(value, oldValue); } removeItem (item: ModelEntryType) { this.value this.value.filter((r) r.name ! item.name); };渲染部分用 Lit 的repeat生成条目并在每个条目内部再渲染一个嵌套分组override render() { return html div classsorter-container ${repeat( this.value, // Note: ideally you have an unique identifier for each item, // but for this example we use the name as identifier. (item) item.name, (item) htmlexample-sorter-item name${item.name} button slotaction click${() this.removeItem(item)}Delete/button example-sorter-group .value${item.children ?? []} change${(e: Event) { item.children (e.target as ExampleSorterGroup).value; }}/example-sorter-group /example-sorter-item, )} /div ; }这里有三层含义.sorter-container就是containerSelector指向的容器repeat渲染的每个example-sorter-item都是该控制器的拖拽条目每个条目内部又嵌了一个example-sorter-group绑定item.children ?? []——这就是嵌套容器父条目的子节点本身也是一个可排序、可拖拽的容器源码注释明确提示用name当唯一标识只是示例方便真实项目应使用真正的唯一 ID。sorter-item.ts 则是条目外观展示名称、操作 slot 与默认 slot供嵌套分组使用并定义了一个umbDragPlaceholder布尔属性映射到drag-placeholder属性property({ type: String, reflect: true }) name: string ; property({ type: Boolean, reflect: true, attribute: drag-placeholder }) umbDragPlaceholder false; // 样式中 // :host([drag-placeholder]) { opacity: 0.2; }当控制器把占位属性加到原条目上时整个条目淡出形成“拖拽占位”的视觉反馈。这个drag-placeholder属性名与控制器源码中placeholderAttr的默认值正好对应因此示例没有配置placeholderAttr也能自动生效。嵌套容器为什么能工作控制器源码机制理解了UmbSorterControllersorter.controller.ts共 1500 余行的几个关键机制示例的行为就都能解释1. 基于 dataTransfer 的“身份家族”识别dragstart时控制器会把两个键写入event.dataTransfertext/umb-sorter-identifier#identifier text/umb-sorter-item-unique#条目的唯一标识目标容器在dragover中执行#obtainIncomingItem只有当dataTransfer.types包含与自身identifier匹配的键时才接收该拖拽并顺带从text/umb-sorter-item-unique#…解析出条目唯一标识。随后控制器会在自己的模型中查找该条目若不存在则尝试onRequestDrop若已存在hasItem为真则拒绝——因此同一个条目不会被重复放进同一容器。这也解释了示例中identifier: string-that-identifies-all-example-sorters的必要性如果每个分组都用默认的Symbol()父容器永远无法识别来自子容器或兄弟容器的拖拽dragover直接被忽略共享同一个字符串后“组内移动”“组间移动”“父子层级间移动”在控制器看来都是同一套协议。2. 事件停止冒泡避免外层容器“抢事件”#itemDraggedOver处理器无论“本容器即当前 drop 目标”还是“本容器成为新的 drop 目标”都会执行e.stopPropagation()。在嵌套结构中这一点至关重要鼠标划过内层容器中的条目时内层容器先处理并阻止事件继续冒泡外层容器不会同时响应从而保证同一时刻只有一个“当前容器”在计算放置位置。3. moveItemInModel本地移动与跨容器移动的统一入口所有放置计算#updateDragMove最终调用#moveElementTo(newIndex)进而调用moveItemInModel(newIndex, fromCtrl)源码约 L1271–L1404。其分支逻辑本地移动fromCtrl this默认实现是对模型数组做splice删除再插入然后触发onChange若配置了performItemMove则交给回调返回false可取消跨容器移动先fromCtrl.removeItem(item)从源容器模型中移除会触发源容器的onChange再在目标容器模型newIndex处插入触发目标容器的onContainerChange带from源控制器引用与onChange并更新静态状态activeSorter/dropSorter/activeIndex指向目标容器纯外部拖放fromCtrl为undefined例如来自其他窗口的原生 drop要求配置requestExternalRemove/requestExternalInsert否则控制台报错并放弃。此外dragend时若dropEffect none如按 ESC 取消控制器会调用moveItemInModel(originalIndex, ...)把条目恢复到拖拽前的位置——这正是originalSorter/originalIndex两个静态字段存在的意义。4. MutationObserver 自动接管动态渲染的条目控制器构造时创建了一个MutationObserver初始化后对容器以{ childList: true, subtree: false }观察凡是新增且匹配itemSelector的节点自动setupItem绑定mousedown/dragstart/dragend、应用ignorerSelector被移除的节点自动destroyItem。因此示例中 Litrepeat随数据变化重新渲染条目时无需手动重新初始化——这是 Web Component 数据驱动渲染与该控制器能配合的根本原因。5. 状态如何沿“嵌套树”回传示例中onChange的写法体现了事件回传链条分组内部模型变化 →onChange更新_value并dispatchEvent(new CustomEvent(change))change事件由分组元素自身派发父级模板直接监听该元素上的change无需冒泡顶层分组 → dashboard 把新数组赋回groupOneItems/groupTwoItemsLit 重渲染后通过.valuesetter 调#sorter.setModel()重新同步控制器嵌套分组 → 父级模板把(e.target as ExampleSorterGroup).value写回item.children下次外层重渲染时同样经valuesetter 同步。配合disable()/enable()、getModel()/hasItem()/getItem()/removeItem()等公开方法控制器就构成了完整的可编程接口。细节与注意事项唯一标识示例用name作为 unique仅适用于数据名称天然唯一的教学场景生产代码务必使用稳定 ID否则跨容器移动会因标识冲突而失败。identifier 的取舍需要跨容器含嵌套容器拖放时才共享 identifier相互隔离的独立列表应保持默认Symbol()可避免互相接收拖拽。占位样式默认drag-placeholder属性即可用 CSS 做出淡出效果如需 class 形式可改用placeholderClass。自动滚动指针靠近滚动容器边缘 50pxautoScrollSensitivity时以每帧 16pxautoScrollSpeed的速度自动滚动源码位于控制器文件头部常量定义约 L5–L6嵌套长列表拖拽时同样生效。源码观察三个示例文件末尾的HTMLElementTagNameMap声明写的是example-sorter-dashboard-nested/example-sorter-group-nested/example-sorter-item-nested与customElement实际注册名不一致从源码结构看这属于复制自嵌套场景时的历史遗留不影响示例运行阅读时以customElement的注册名为准。测试与相关实现控制器的单元测试位于 sorter.controller.test.ts控制器经由 sorter 包的 index.ts 从umbraco-cms/backoffice/sorter导出即示例中import { UmbSorterController } from umbraco-cms/backoffice/sorter的来源示例包的运行方式见 package.json 中的example/example:mock脚本。小结这个 4 个文件的小示例完整覆盖了 Umbraco Backoffice 接入UmbSorterController的标准姿势manifest 注册 → 状态容器元素 → 控制器配置unique 解析、identifier、item/container 选择器、onChange→ 数据驱动的 repeat 渲染 → 占位样式。其嵌套能力的本质是共享 identifier 让所有 Sorter 实例讲同一种 dataTransfer 协议事件 stopPropagation 保证嵌套时层级间互不干扰MutationObserver 保证动态 DOM 与控制器状态自动同步。把这三点理解清楚后无论是内容类型属性里的组件排序、还是自定义 workspace 中的多级资源组织界面都可以按同样的模式实现。赞分享CMS后端【免费下载链接】Umbraco-CMSUmbraco is a free and open source .NET content management system helping you deliver delightful digital experiences.项目地址https://gitcode.com/gh_mirrors/um/Umbraco-CMS点击查看免费下载相关推荐Ant Design Table 拖拽排序进阶基于 dnd-kit 实现带拖拽手柄Handler的行排序Ant Design Table 拖拽排序进阶基于 dnd kit 实现带拖拽手柄Handler的行排序 本篇文章围绕 drag sorting hand前端UI组件设计系统React DnD 可排序列表实战用 useDrag useDrop 在 hover 中实现拖拽排序Simple 示例深度解析React DnD 可排序列表实战用 useDrag useDrop 在 hover 中实现拖拽排序Simple 示例深度解析 React DnD 为前端UI组件rsuite List 组件 collection 分组排序实战基于集合隔离的拖拽排序实现与源码解析rsuite List 组件 collection 分组排序实战基于集合隔离的拖拽排序实现与源码解析 本文基于 rsuite 官方文档 collection.前端UI组件上一篇exa现代ls命令的革命性替代品下一篇Mako 开源项目教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

半导体和芯片究竟啥区别?一文讲透材料、器件与电路

半导体和芯片究竟啥区别?一文讲透材料、器件与电路

如果你在半导体行业待过一阵子,一定遇到过这种对话:有人问你做什么的,你说做芯片的,对方追问芯片和半导体是不是一回事,你一时竟不知道从哪句开始解释。这个问题看着基础,其实特别容易把人绕进去。我见过不…

2026/10/13 9:15:33 阅读更多 →
2026年AI证书怎么选?三张高含金量认证与避坑指南

2026年AI证书怎么选?三张高含金量认证与避坑指南

2026年聊AI证书,千万别聊成"考证热"那套。我这两年被HR朋友拉去当技术面试官,明显感觉到一个变化:简历上堆一堆"精通ChatGPT""提示词十级"这种东西,现在基本没有任何杀伤力了。面试官早就看腻了。真…

2026/10/13 9:15:33 阅读更多 →
运维工程师面试高频题深度拆解:从Linux排查到Kubernetes实战

运维工程师面试高频题深度拆解:从Linux排查到Kubernetes实战

1. 运维面试到底在考什么:先看懂题目背后的能力模型说句大实话,很多准备运维工程师面试的朋友,简历投了一大堆,刷题也刷了不少,但一到面试现场就露怯。问题往往不在于知识点记没记住,而在于压根没搞清楚面试…

2026/10/13 9:15:33 阅读更多 →

最新新闻

PowerContext挂接MiniMax Code:终结跨会话与跨Agent上下文断裂

PowerContext挂接MiniMax Code:终结跨会话与跨Agent上下文断裂

大约两个星期前,我把手头的项目从“上下文完全靠每次手动贴”的方式,切到了 PowerContext 挂接 MiniMax Code 的方式。困扰我很久的一个问题终于不再是问题:跨会话和跨 Agent 之间的项目上下文,不再断裂了。起因很简单。我开一个新…

2026/10/13 10:00:16 阅读更多 →
领域模型进化:GRPO 强化学习与 Post-training Engineering

领域模型进化:GRPO 强化学习与 Post-training Engineering

通用大模型(base model)是个通才。它在万亿 token 上做过 next-token prediction,把世界知识压缩进了权重,但你让它写一段符合你公司合规规范的代码、审一份你领域的合同、跑通你内部系统的 SQL,它大概率会给你一个&qu…

2026/10/13 10:00:16 阅读更多 →
t3code 深度拆解:编码工具核心原理与实操避坑指南

t3code 深度拆解:编码工具核心原理与实操避坑指南

1. 从“t3code”这个标题说起:它到底是什么,能解决什么问题第一次看到“t3code”这个标题,很多人会一头雾水。它不像“Python爬虫实战”那样直白,也不像“家庭咖啡烘焙指南”那样有明确场景。但恰恰是这种模糊性,给了我…

2026/10/13 10:00:16 阅读更多 →
Seldon Core Pipeline 就绪状态检查实战:从 tfsimples 示例到 ready 接口的源码解析

Seldon Core Pipeline 就绪状态检查实战:从 tfsimples 示例到 ready 接口的源码解析

人工智能大模型MLOpsLLMOps模型推理服务云原生后端 【免费下载链接】seldon-core An MLOps framework to package, deploy, monitor and manage thousands of production machine learning models 项目地址: https://gitcode.com/gh_mirrors/se/seldon-core 点击查看…

2026/10/13 10:00:16 阅读更多 →
Oracle到达梦迁移实战:数据类型、SQL语法与存储过程避坑指南

Oracle到达梦迁移实战:数据类型、SQL语法与存储过程避坑指南

1. 迁移之前,先把两边的“家底”摸清楚1.1 为什么Oracle到达梦的迁移总在“意料之外”翻车做过数据库迁移的人都有一个共识:迁移本身不难,难的是迁移之后业务跑不起来。Oracle到达梦的迁移尤其如此,因为这两套数据库虽然都支持SQL…

2026/10/13 10:00:16 阅读更多 →
Java垃圾分类系统:树形分类建模与时间窗调度实战

Java垃圾分类系统:树形分类建模与时间窗调度实战

简介:这是一套已高分通过的Java毕业设计项目源码,面向计算机专业本科生及Java初学者,聚焦城市垃圾分类回收管理场景,提供从用户注册登录、垃圾类型分类录入、回收点信息维护、预约回收申请到后台数据统计的完整业务闭环&#xff0…

2026/10/13 9:59:14 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 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/12 21:05:09 阅读更多 →
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/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练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/12 18:11:17 阅读更多 →