news 2026/10/24 5:11:28

K3Cloud插件开发入门:BOS扩展机制与动态表单事件实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
K3Cloud插件开发入门:BOS扩展机制与动态表单事件实战

简介:面向金蝶K/3 Cloud BOS平台初学者的插件开发入门文档,聚焦BOSIDE、套打、万能报表等常用场景,讲解如何借助BOS动态领域模型扩展标准业务,实现企业个性化和非标准化需求。文档先梳理开发环境配置,涵盖Windows平台、Visual Studio 2012、SQL Server 2008R2或Oracle 11g数据库,并特别说明SQL Server排序规则需设为Chinese_PRC_CI_AS、Oracle字符集需设为AL32UTF8,同时给出公共环境与个人环境的搭建要点,避免新手踩坑。插件部分重点介绍四类插件:动态表单插件处理单据事件与操作,是最常用类型;服务插件对外提供编程接口;校验规则插件负责业务规则校验;表单构建插件可动态调整界面控件与元数据。开发流程则完整演示了新建工程、添加Kingdee.BOS引用、创建继承AbstractBillPlugIn的插件类、重载BarItemClick方法、设置编译路径到服务器Bin目录、通过IDE设计器注册插件并运行测试,覆盖编码到部署的关键步骤。资源为单个DOCX文档,压缩包大小1.9MB,共1个文件,结构清晰,适合初学者边看边练。目前已有216人学习下载,可作为快速上手BOS插件开发的入门参考资料。

1. k3cloud 插件开发:先搞懂这套扩展机制解决什么问题

做 K/3 Cloud 二开的同学应该都有这种体会:标准产品能覆盖企业七八成的业务,剩下的零头才是真正让你加班的东西——审批流里要按供应商信用等级自动分单、单据保存前要回写一套外部系统的对账标记、界面上要多一个按物料分类批量修改的按钮。这些需求说大不大,但在标准产品里就是找不到入口。BOS 平台的思路很直接:用动态领域模型把业务拆成标准接口和标准服务,再用插件机制留出扩展位。插件可以拦截事件、覆盖操作、调整界面元数据,相当于在封闭产品上开了一扇门。

本文要拆的这份《初学者-k3cloud插件开发基础》笔记,内容覆盖环境搭建、BOS 架构模型、四类插件选型、动态表单插件的事件生命周期和具体代码示例,适合刚接触 BOS 二开的实施顾问和开发者。它不教你业务建模,只讲一件事:插件怎么落地。下面按我实际复现的顺序,从环境准备讲到避坑,最后给一段可抄的进阶代码。

2. 开发环境搭建:从数据库到编译路径的完整清单

2.1 服务器与客户端配置基准先定下来

BOS 插件开发跑在 Windows 平台上,开发工具是 Visual Studio,这点没有商量的余地。笔记里给了一套最低配置建议,我复现的时候按这个基准来,基本没碰到性能瓶颈:

角色CPU内存备注
团队数据库服务器4 核 2.0 GHz 以上SQL Server 8 GB / Oracle 16 GB数据库服务单独部署
客户端双核 2.0 GHz 以上推荐 4 GB只装开发工具和客户端
个人客户端 + 数据库双核 2.0 GHz 以上SQL Server 4 GB / Oracle 8 GB本地联调用

服务器运行环境方面,数据库服务器要求 SQL Server 2008R2 以上或 Oracle 11g 以上,Web 服务器要求 IIS 6.0 以上、Windows Server 2008 以上,.NET Framework 4.0 是硬依赖。客户端环境比较特殊的地方在于浏览器:IE 8.0 / 9.0 需要 Silverlight 5 插件,Firefox 12 以上只支持 32 位 Windows。这里要注意,K/3 Cloud 早期版本的界面依赖 Silverlight,如果浏览器版本不对,插件页面直接白屏,连登录都进不去。

有一个配置容易被忽略:SQL Server 2008 的排序规则必须是Chinese_PRC_CI_AS,Oracle 的数据库字符集必须是AL32UTF8,国家字符集必须是AL16UTF16。字符集不对的后果很隐蔽——插件里查出来的中文是乱码,单据保存后数据错位,而且这种问题在开发环境不一定能复现,往往要到上线前数据迁移才暴露。我一般会在装库之后第一时间用查询语句确认字符集,而不是等报错再去排查。

2.2 公共环境与个人开发环境分开搭

笔记把环境分成公共环境和个人环境两层,这个划分是有道理的。公共环境是一台独立的数据库服务器加一台 Web 服务器,Web 服务器上装 K/3 Cloud 产品并配置为管理中心站点,团队共享这套环境做集成测试。个人环境则简单得多:自己机器上装 Visual Studio 和 K/3 Cloud 产品,但不需要配置管理中心,开发调试时连到公共环境的管理中心即可。

个人环境连管理中心这一步,是靠改配置文件实现的。打开 K/3 Cloud 安装目录下的K3Cloud\K3CloudServer\App_Data\Common.config,找到managementSiteUrl这个键,把地址改成公共环境管理中心的 IP 和端口:

<add key="managementSiteUrl" value="http://192.168.73.40:8000/" />

改完配置后要重启 IIS 或对应应用池才能生效。这个managementSiteUrl是个人开发环境访问公共管理中心的路由入口,如果填错或者端口不对,打开 K/3 Cloud 客户端时会一直转圈,日志里只有连接超时的报错。我习惯在改完配置后先 ping 一下目标 IP,再用浏览器直接访问http://192.168.73.40:8000/确认管理中心页面能打开,再做下一步。

2.3 开发工具链与插件编译输出路径

开发工具这块,笔记列得比较明确:Visual Studio 2012 是主开发环境,IE 需要 Silverlight 5,数据库用 SQL Server 2008R2 或 Oracle 11G R2,跟踪工具用 HttpWatch Pro 6.0。这里面 Visual Studio 2012 是个老版本,如果你机器上装的是更高版本的 VS,建项目时选择 .NET Framework 4.0 目标框架也能兼容,但要注意插件引用的程序集版本必须和服务器上的运行时一致,否则编译出来的 DLL 在服务器上加载时会报程序集版本不匹配。

插件编译输出路径是一个关键点:编译好的组件要放到K3Cloud\K3CloudServer\Bin\目录下。这个路径就是 K/3 Cloud 服务器运行时加载插件的目录,IDE 设计器在注册插件时也是从这个目录去浏览选择组件的。如果不把编译输出路径设置成这个目录,每次编译完都要手动复制 DLL,而且容易搞混版本。我会在项目属性里直接把输出路径写死到服务器 Bin 目录,编译完直接注册,省掉复制这一步。

3. BOS 架构与四类插件:接到需求先判断用哪一类

3.1 动态表单模型:View 管外观,Model 管逻辑

BOS 架构模型的核心是动态表单模型,它把表单拆成两个面:外观和业务逻辑。外观由 View(视图)控制,负责界面控件样式、菜单位置、布局展示;业务逻辑由 Model(模型)控制,负责服务、校验器、操作和业务规则。这两个面的数据分别存在布局元数据(LayoutInfo)和业务元数据(BusinessInfo)里,在 IDE 设计器里设置的东西最终都会落到这两份元数据中。

View 和 Model 分别提供了两组接口:IDynamicFormView是插件可以访问的视图接口,包含元数据、多视图模型接口、操作转发指令和通用属性方法;IDynamicFormViewService是动态表单内部用的接口,包含 Controller 消息路由方法,二次开发不需要关注。Model 侧同理,IDynamicFormModel是插件可访问的模型接口,IDynamicFormModelService是内部接口。这个设计说白了就是把对外扩展点和内部实现隔离开,保证插件调用的是稳定接口,不会因为内部实现调整而翻车。

理解了这个分层,你就知道为什么有些操作要写在AfterBindData里、有些要写在DataChanged里——AfterBindData在绑定数据及控件状态时触发,适合控制界面可见性;DataChanged只在字段值改变后触发,适合做字段联动逻辑。写错事件,效果就差很远。

3.2 四类插件的分工与选型逻辑

BOS 平台提供了四类插件,接到需求先判断属于哪一类,不要一上来就写动态表单插件:

插件类型用途典型场景
动态表单插件业务逻辑封装,介入单据事件、操作和服务单据保存前校验、按钮点击响应
服务插件基于动态表单服务端的插件,提供服务编程接口外部系统直接调用服务接口
校验规则插件针对业务规则进行校验字段合法性、必录项检查
表单构建插件对界面加载控件和元数据做调整动态控制控件显示、修改元数据

动态表单插件是最常用的类型,它负责单据上各种业务事件、操作和服务的介入。服务插件则面向外部服务直接调用的场景,比如第三方系统通过服务接口向 K/3 Cloud 写入数据,这时候走的就是服务插件。校验规则插件用途比较单一,就是在业务规则层做校验。表单构建插件更底层一些,能对界面控件和元数据做动态调整,适合处理特殊界面需求。

判断逻辑很简单:如果是界面上按钮、事件、数据的处理,用动态表单插件;如果是外部系统调服务接口,用服务插件;如果只是做规则校验,用校验规则插件;如果要动界面控件本身的加载行为,才考虑表单构建插件。这个选型判断做对了,后面开发会顺很多;做错了,比如把服务接口逻辑塞进动态表单插件里,调试的时候要找半天入口。

3.3 插件继承体系与命名空间速查

动态表单插件分四类:单据插件、基础资料插件、动态表单插件、列表插件。继承关系为AbstractDynamicFormPlugIn是基类,单据插件继承自AbstractBillPlugIn,基础资料插件继承自AbstractBasePlugIn,列表插件继承自AbstractListPlugIn。写插件时先想清楚目标单据属于哪一类,选错基类的话,事件不会触发。

命名空间集中在Kingdee.BOS.Core.DynamicForm.PlugIn,核心类包括:

类 / 接口说明
AbstractDynamicFormPlugIn动态表单插件抽象基类
AbstractBillPlugIn单据插件抽象基类
AbstractBasePlugIn基础资料插件抽象基类
AbstractListPlugIn列表插件抽象基类
IDynamicFormViewPlugIn视图层插件接口,接收 View 层事件
IDynamicFormModelPlugIn模型层插件接口,接收 Model 层事件

视图层接口提供了BarItemClick、ButtonClick、AfterBindData、BeforeBindData、OnInitialize等一系列事件方法,模型层接口则提供CreateNewData、AfterCreateNewEntryRow、DataChanged、BeforeUpdateValue等方法。写插件时如果要覆盖某个事件,先查一下它属于 View 层还是 Model 层,心里有个底,因为这两层事件的触发时机不一样,调试定位问题的思路也不同。

4. 动手写第一个插件:HelloWorld 到动态表单事件

4.1 建项目、引用组件、重载 BarItemClick

插件开发的第一个例子是做一个 HelloWorld 菜单,点击后弹出一个对话框。步骤很固定:打开 Visual Studio 2012,新建工程MyDev.K3.SCM.Stock.Business.PlugIn,添加Kingdee.BOS和Kingdee.BOS.Core两个引用组件,然后新建一个类ReceiptEdit,继承自AbstractBillPlugIn。

using Kingdee.BOS.Core.Bill.PlugIn; using Kingdee.BOS.Core.DynamicForm; using Kingdee.BOS.Core.DynamicForm.PlugIn.Args; namespace MyDev.K3.SCM.Stock.Business.PlugIn { public class ReceiptEdit : AbstractBillPlugIn { public override void BarItemClick(BarItemClickEventArgs e) { base.BarItemClick(e); if (e.BarItemKey == "HelloWorld") { this.View.ShowMessage("Hello world!", MessageBoxType.Notice); } } } }

这段代码的逻辑是重载BarItemClick事件,当用户点击标识为HelloWorld的按钮时,弹出一个提示框。base.BarItemClick(e)必须保留,否则父类的默认处理不会执行。e.BarItemKey是按钮的标识,这个值要和 IDE 设计器里按钮的标识属性一致,大小写敏感。如果点击没反应,先检查按钮标识是不是写错了——这是新手最容易踩的坑,不是代码有问题,而是标识对不上。

4.2 编译输出到 Bin 目录,再进 IDE 设计器注册

代码写完后,要把编译路径设置到K3Cloud\K3CloudServer\Bin,然后编译。接着打开 IDE 设计器,找到目标单据的属性窗口,编辑“采购收料单”单据属性,在插件列表界面点击“注册插件”。这里有个提醒:插件列表里可能已经有其他插件,这些插件在运行时会动态加载,删除它们可能会导致业务数据错误。注册时选择编译好的组件,勾选插件,确定返回后保存单据。

注册插件这一步是二开和普通代码开发的区别所在——代码编译出来只是半成品,必须在 IDE 设计器里把插件挂到目标单据上,运行时引擎才会加载。我见过有人代码写完、编译通过,但忘记注册,结果怎么测都没反应,最后发现插件根本没挂上去。注册完成后要重新打开单据页面,插件才会生效。

4.3 加载机制:插件事件的触发顺序

动态表单第一次访问时,先加载元数据,初始化视图和模型对象,初始化页面,然后创建数据包并绑定数据。插件的执行顺序如下:

事件触发时机
OnInitialize页面初始化
CreateNewData动态表单数据包创建(仅新增时触发)
AfterCreateNewEntryRow创建分录行后
AfterCreateNewData动态表单数据包创建后
BeforeBindData绑定数据前
AfterBindData绑定数据及控件状态后
BeforeClosed页面关闭前

理解这个顺序很重要。比如你需要在打开单据时给某个字段设置默认值,应该写在AfterCreateNewData里,因为这时候数据包已经创建完成,字段可以赋值;如果写在OnInitialize里,数据包还没创建,拿不到数据对象。反过来,如果要控制界面控件的可见性,就要写在AfterBindData里,因为此时界面已经绑定完数据和控件状态,改样式才有意义。

4.4 常用事件:AfterCreateNewData 与 AfterBindData 实战

AfterCreateNewData用于在新增单据时根据特殊需求修改当前数据。下面例子是新增单据时根据当前组织获取邮件的默认设置,赋值到数据包中:

public override void AfterCreateNewData(EventArgs e) { base.AfterCreateNewData(e); OQLFilter ofilter = new OQLFilter(); ofilter.Add(new OQLFilterHeadEntityItem { FilterString = string.Format(" FORGID ={0} ", this.Model.Context.CurrentOrganizationInfo.ID) }); DynamicObject[] obj = BusinessDataServiceHelper.Load( this.View.Context, "BAS_MAILDEFAULTSET", null, ofilter); if (obj != null && obj.Count() > 0) { DynamicObject defaultSet = obj[0]; this.View.Model.SetValue("FMessageType", defaultSet["FMessageType"]); this.View.Model.SetValue("FServer", defaultSet["FOutgoingMailServer"]); this.View.Model.SetValue("FSMTPPort", defaultSet["FSMTPPort"]); } }

这段代码的思路是:通过OQLFilter构造一个按当前组织 ID 过滤的查询条件,调用BusinessDataServiceHelper.Load加载邮件默认设置,取第一条数据,把邮件类型、服务器、端口三个值赋到当前表单字段上。SetValue的第二个参数是字段标识,要确保和 IDE 设计器里的字段标识一致。这段逻辑写在AfterCreateNewData里的原因是:新增时数据包刚创建,此时赋值不会触发DataChanged事件,避免产生不必要的联动逻辑。

AfterBindData是另一个高频事件,加载和界面刷新都会调用它,通常用来处理数据可见性等界面状态。比如根据单据类型隐藏主菜单的“新增”按钮:

public override void AfterBindData(EventArgs e) { base.AfterBindData(e); // 隐藏主菜单上的“新增”按钮 this.View.GetMainBarItem("tbNew").Visible = false; // 启用分录菜单里的“添加行” this.View.GetBarItem("FEntity", "tbAdd").Enabled = true; }

GetMainBarItem获取的是主菜单按钮,GetBarItem获取的是分录菜单按钮,第一个参数是分录标识,第二个参数是按钮标识。这两个方法返回的是工具栏项对象,可以修改Visible和Enabled属性。需要注意的是,AfterBindData在界面每次刷新时都会触发,不要在它里面做耗时操作,否则界面会卡顿。

5. 避坑指南:环境配置与插件调试的常见问题

5.1 数据库字符集不对,查询出来的中文全是乱码

现象:插件里通过BusinessDataServiceHelper加载的数据,中文显示为乱码,但客户端界面本身的菜单和单据名称显示正常。

原因:SQL Server 排序规则不是Chinese_PRC_CI_AS,或者 Oracle 字符集不是AL32UTF8。BOS 平台在读写数据时依赖特定的字符集编码,字符集不匹配时,中文文本在数据库和应用程序之间转换就会失真。

解决:装库之后立即检查字符集。SQL Server 执行SELECT DATABASEPROPERTYEX('你的数据库名', 'Collation'),确认返回Chinese_PRC_CI_AS;Oracle 执行SELECT VALUE FROM NLS_DATABASE_PARAMETERS WHERE PARAMETER = 'NLS_CHARACTERSET',确认字符集为AL32UTF8。如果不对,需要在装库时指定字符集重装,不要等到数据迁完再去改,那时候代价就大了。

5.2 编译出的 DLL 在 IDE 里浏览不到

现象:IDE 设计器里注册插件时,点击浏览按钮,找不到刚才编译好的组件。

原因:编译输出路径没有设置到K3Cloud\K3CloudServer\Bin,或者编译时目标框架与服务器运行时不一致。IDE 设计器默认从 Bin 目录加载插件程序集,路径不对自然看不到。

解决:在 Visual Studio 项目属性里把编译输出路径直接设置为K3Cloud\K3CloudServer\Bin,重新编译后再去注册。同时检查目标框架是否为 .NET Framework 4.0,版本不对也会导致程序集加载失败。

5.3 个人环境连不上管理中心

现象:个人开发环境打开 K/3 Cloud 客户端,一直转圈或提示连接超时,日志里看不到明确报错。

原因:Common.config里的managementSiteUrl指向的 IP 或端口不对。这个地址是个人环境访问公共管理中心的唯一入口,填错了整个客户端都起不来。

解决:打开K3Cloud\K3CloudServer\App_Data\Common.config,检查managementSiteUrl的值。改完之后不要急着开客户端,先用浏览器直接访问这个地址,确认管理中心页面能正常打开,再启动客户端。如果端口不通,检查防火墙或者 IIS 站点是否启动。

5.4 删除了插件列表里的系统插件,单据业务报错

现象:注册自己插件的时候,看到列表里有其他插件,觉得没用就删掉了,之后单据打开或保存时报业务错误。

原因:插件列表里除了自己的插件,可能还有系统内置的标准插件。这些插件在运行时负责标准业务逻辑,删除后标准服务不执行,单据的基础行为就乱了。

解决:注册插件时只操作自己新增的条目,不要动既有的插件。如果已经误删,找一台装过同样版本的机器,对比插件列表,把缺失的插件重新注册回去。最稳妥的做法是:在动任何既有插件之前,先把插件列表截图保存,方便回退。

5.5 插件没生效,不知道是没注册还是没编译

现象:写好的插件代码编译通过,注册也做了,但运行单据时事件没有触发。

原因:最常见的情况是编译输出路径和注册时选择的组件不是同一个文件——比如编译到了本地目录,注册时手动浏览到了 Bin 目录里的旧版本 DLL,导致注册的是老代码。

解决:每次编译后确认K3Cloud\K3CloudServer\Bin下的 DLL 时间戳是否有更新。我一般会在编译前先删除 Bin 目录里对应的旧 DLL,编译后检查生成时间,再进 IDE 注册。如果改了代码但 Bin 目录里的文件没变,说明输出路径没设对,先从路径查起。

6. 进阶实战:用 OnInitialize 动态控制界面加载

讲一个实际用得上的技巧:批量修改界面初始化时,需要把允许批量修改的字段动态加载到下拉列表里。这种场景下字段列表不是写死的,而是根据元数据中的字段属性动态生成,写在OnInitialize里最合适。

public override void OnInitialize(InitializeEventArgs e) { // 根据列表的 formid,获取元数据 FormMetadata metadata = (FormMetadata)ServiceHelper.MetaDataServiceHelper.Load( this.View.Context, this.View.ParentFormView.BillBusinessInfo.GetForm().Id); // 设置界面标题 string strTitle = string.Format("{1}-[{0}]", metadata.GetLayoutInfo().GetFormAppearance().Caption, this.View.LayoutInfo.GetFormAppearance().Caption); LocaleValue formTitle = new LocaleValue(); formTitle.Add(new KeyValuePair<int, string>(this.Context.UserLocale.LCID, strTitle)); this.View.SetFormTitle(formTitle); List<EnumItem> list = new List<EnumItem>(); // 循环检测哪些字段允许批量修改,加入下拉列表 foreach (Field field in metadata.BusinessInfo.GetFieldList()) { if ((field.FunControl & Field.FUNCONTROL_BULK_EDIT) != Field.FUNCONTROL_BULK_EDIT) continue; Appearance app = metadata.GetLayoutInfo().GetAppearance(field.Key); if (app != null) { if ((app.Visible & Appearance.VIS_EDIT) != Appearance.VIS_EDIT) continue; } EnumItem item = new EnumItem(); item.Value = field.Key; item.Caption = field.Name; list.Add(item); } list = list.OrderBy(p => p.Caption[this.View.Context.UserLocale.LCID]).ToList(); if (list.Count != 0) { selectedFielKey = list.FirstOrDefault().Value; } this.View.GetControl<ComboFieldEditor>("FCombo").SetComboItems(list); }

这段代码做了三件事:加载元数据并设置表单标题;遍历业务元数据中的字段列表,筛选出允许批量修改且编辑状态下可见的字段;把筛选结果按当前语言排序后填充到下拉控件FCombo中。关键点在于两个位运算判断——field.FunControl & Field.FUNCONTROL_BULK_EDIT检查字段是否允许批量修改,app.Visible & Appearance.VIS_EDIT检查字段在编辑状态下是否可见。这两个条件缺一不可,否则会把只读字段和隐藏字段也塞进下拉框,用户选了也没意义。

验证这个方法是否生效,我在实践中用两个手段:一是在OnInitialize里打断点,看list.Count的值和 IDE 设计器里字段属性是否一致;二是用 HttpWatch Pro 6.0 监控客户端发起的 HTTP 请求,确认表单元数据加载成功后,下拉列表的数据是否随请求返回。从那以后,我每次写界面动态加载逻辑,都强制自己先确认元数据字段属性,再动手写循环筛选,省掉大量调试时间。这份笔记对刚接触 BOS 插件开发的人来说,能把从环境到事件的完整路径一次讲清,希望帮到你。

本文还有配套的精品资源,点击获取

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

使用云服务器通过宝塔面板部署经典若依(前后端分离)项目

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

作者头像 李华
网站建设 2026/10/24 5:09:35

从PMIC到8位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/24 5:09:29

从MOG2到BackgroundMatting:运动目标检测技术路线全解析

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

作者头像 李华
网站建设 2026/10/24 5:08:02

时区划分的底层逻辑:从UTC、夏令时到跨时区协作与编程实践

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

作者头像 李华
网站建设 2026/10/24 5:07:44

STC芯片命名规则全解析:从丝印到选型避坑指南

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

作者头像 李华
网站建设 2026/10/24 5:05:35

MATLAB航空航天工具箱R2023b核心用法:坐标系、DATCOM与卫星场景

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

作者头像 李华