如何在Python开发中实现无代码、纯配置的业务界面展示和常规数据操作的处理分析过程
<p>要实现无代码、纯配置的业务界面展示和常规数据操作,最佳的方式是通过实体-属性-值的设计方式,也就是常说的EAV模式,通过动态构建实体类型、动态构建对应的属性列表,以及根据类型的不同对属性值进行存储,从而构建一系列的处理规则,实现业务模块的动态化,本篇随笔探讨一下,如何在Python开发中实现无代码、纯配置的业务界面展示,以及实现常规数据操作的过程,抛砖引玉,共同探讨。</p><h3>1、何为实体-属性-值的设计方式</h3>
<p>EAV(Entity-Attribute-Value)模型,我们先来了解一下。</p>
<p>EAV 把所有业务抽象成:</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203120435292-47954550.png" alt="image" width="670" height="192" loading="lazy"></p>
<p>数据结构示例,如下所示:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">Entity: Customer
Attributes:
name </span><span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)">
phone </span><span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)">
level enum
Values:
(</span><span style="color: rgba(128, 0, 128, 1)">001</span>, name, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">张三</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
(</span><span style="color: rgba(128, 0, 128, 1)">001</span>, phone, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">138****</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
(</span><span style="color: rgba(128, 0, 128, 1)">001</span>, level, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">VIP</span><span style="color: rgba(128, 0, 0, 1)">"</span>)</pre>
</div>
<p>分别 包括定义实体表,对应的属性类别(名称、类型等),以及每个属性的值记录。</p>
<p>一句话说明就是:每个实体都有唯一的标识符,每个实体都可以有多个属性与之关联,每个属性都有唯一的标识符,每个属性都可以具有多个值。</p>
<p>我们对<strong>属性值</strong>表基于数据类型进行分割,每个不同的数据类型拆为一个单独的表,同时通过 属性表(Attribute) 添加 类型决定去哪里存取数据。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202405/8867-20240514114133990-1331499807.png" alt="" width="350" height="454" class="medium-zoom-image" loading="lazy"></p>
<p> 我们可以借鉴magento的eav模型,它是EAV设计的最优参考了。Magento 2中的EAV属性类型有下面这些表:</p>
<ul class="ul-level-0">
<li>eav_entity_int</li>
<li>eav_entity_varchar</li>
<li>eav_entity_text</li>
<li>eav_entity_decimal</li>
<li>eav_entity_datetime</li>
</ul>
<p>属性元数据驱动 UI 控件选择,我们也根据属性的类型进行定义,如可以定义不同的输入控件。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203131456754-1172982545.png" alt="image" width="696" height="219" loading="lazy"></p>
<p> 一句话,EAV + 元数据 + 通用UI + CRUD引擎 = 无代码业务系统</p>
<p data-start="5376" data-end="5381">本质就是:</p>
<ul data-start="5383" data-end="5473">
<li data-start="5383" data-end="5399">
<p data-start="5385" data-end="5399"><strong data-start="5385" data-end="5399">业务结构:EAV建模</strong></p>
</li>
<li data-start="5400" data-end="5420">
<p data-start="5402" data-end="5420"><strong data-start="5402" data-end="5420">界面构建:属性→控件自动映射</strong></p>
</li>
<li data-start="5421" data-end="5438">
<p data-start="5423" data-end="5438"><strong data-start="5423" data-end="5438">数据交互:通用CRUD</strong></p>
</li>
<li data-start="5439" data-end="5455">
<p data-start="5441" data-end="5455"><strong data-start="5441" data-end="5455">规则校验:DSL配置</strong></p>
</li>
<li data-start="5456" data-end="5473">
<p data-start="5458" data-end="5473"><strong data-start="5458" data-end="5473">查询过滤:条件模板驱动</strong></p>
</li>
</ul>
<p> </p>
<h3>2、在Python开发中实现无代码、纯配置的业务界面展示和常规数据操作</h3>
<p>有了上面的EAV知识的介绍,我们可以来进一步探讨在Python开发中实现无代码、纯配置的业务界面展示和常规数据操作过程了。</p>
<p>我们先来看一个界面下效果。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203132408878-968549868.png" alt="image" width="1235" height="615" loading="lazy"></p>
<p>对于这样一个常规的列表展示界面,包括有条件查询、分页列表记录展示,属性类型,包括文本、整数、浮点小数、日期、备注长文本等类型录入,以及对应不同的数据类型,有下拉列表(固定的、动态字典的)、复选框、评分、弹出选择、映射关联属性等多种方式的录入处理,也是比较常见的情况。</p>
<p>而对于条件,可以展开多个条件,展开效果如下所示。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203132844552-848998970.png" alt="image" width="1224" height="177" loading="lazy"></p>
<p> 而对于数据的录入,有弹出界面处理方式,也有对应直接编辑列表的方式,直接编辑输入比较快捷,如果能够丰富录入的控件处理,那么也是非常好的一种数据编辑方式。</p>
<p>因此我们直接在列表中进行数据的编辑处理,提供不同类型、不同方式的输入处理,如下面是动态字典,通过下拉列表或者单选框的方式进行录入。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203133402867-2005056058.png" alt="image" width="346" height="353" loading="lazy"></p>
<p>而对于一些系统用户、角色、机构,我们应该也可以弹出来选择记录,并更新关联的字段信息</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203134217147-2109041588.png" alt="image" width="772" height="344" loading="lazy"></p>
<p>列表界面一般为了方便,会提供相关的右键菜单,提供常规的操作处理。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203135121704-410139637.png" alt="image" width="488" height="389" loading="lazy"></p>
<p> 而有些业务表是主从表的方式进行展示,如对应报价单、订单等常规的数据,通过主从表的方式会更加合适。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203134715758-483551667.png" alt="image" width="1040" height="525" loading="lazy"></p>
<p> 主表可以直接录入外,明细表也通过直接录入的方式,通过选择产品,可以快速的实现数据记录的选择,以及对相关字段属性值的复制,非常方便。</p>
<p> </p>
<h3> 3、配置业务界面处理</h3>
<p>如果要实现上面业务界面的展示处理,那么我们需要如何配置业务界面元素呢。</p>
<p>通过上面的EAV介绍</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203120435292-47954550.png" alt="image" width="670" height="192" loading="lazy"></p>
<p>那么我们至少围绕上面几个信息来定义和存储数据内容,在更高的层次上定义好相关的信息:</p>
<p>1)实体类是否分页、是否有主从表关系。</p>
<p>2)属性定义,需要包括是否可以查询(作为条件)、是否可用(显示与否)、属性类型(决定存储位置)、控件输入方式(日期、数值、文本),而其中文本最为灵活,可能是通过配置字典(动态或者固定列表),选择系统表方式获取,选择动态业务表对象,通过编码规则生成编码等方式,数值可能是常规数值输入、评分输入、或者复选框等方式。另外还有是否必填、是否只读,排序顺序等关键定义</p>
<p>3)属性值的存储,根据不同的数据类型,存储在不同的表中,提高处理效率的同时不会降低精度。</p>
<p>4)属性信息的提取,这个非常关键,如果把这些数据每次组合起来,那么常规的做法就是关联多个表来实现数据的联合,但是效率会非常低下。好的做法可以利用NoSQL的动态文档的特点,对数据的组合通过MogoDB的方式实现快速检索处理,存储的时候,一份完整的记录存储在MongoDB,另外一份数据写入具体的属性值表中,必要时可以随时实现同步即可。</p>
<p>有了上面的几点介绍,我们来看看具体在Python中如何管理这些内容。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203141646612-1489442585.png" alt="image" width="970" height="572" loading="lazy"></p>
<p>如上面顶部为实体(或实体类型)的定义信息,主要包括名称、模型类名、是否分页几个属性。</p>
<p>下面是对应实体类型的属性列表,其中属性名称、模型类型名、存储类型,为核心信息,其他必填、排序、字典类型、只读、隐藏、可查询 等等属性定义为一些构建界面必须的相关属性。</p>
<p>这两个表可以通过直接编辑模式进行快速录入,从而方便动态定义实体类型和相关的属性列表。</p>
<p>另外通过定义从表,可以从系统的动态定义实体类型中选择业务表作为从表信息,如下对于订单或者报价单的业务,通过主从表的方式显示的,定义界面如下所示。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203142117114-739765232.png" alt="image" width="1069" height="630" loading="lazy"></p>
<p> 而对于一些属性字段的输入类型,我们提供一些内置的选项供选择。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203142440938-2118497637.png" alt="image" width="847" height="292" loading="lazy"></p>
<p>如前面介绍的选择用户方式,就从基础用户表中选择记录,更新关联的字段信息。</p>
<p>而如果选择类似系统业务编码的,那么也提供一个编码生成的方式(结合业务编码模块规则生成编码),如订单中的订单编码记录,新增的时候,提供一个按钮可以结合订单编码规则生成编码。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203142744084-2010460878.png" alt="image" width="692" height="89" loading="lazy"></p>
<p> 而对于常规的字典,我们可以通过配置字典类型,就可以实现字段和系统字典项目的关联了。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203143326623-2083583486.png" alt="image" width="636" height="168" loading="lazy"></p>
<p> 这样就可以在实际记录的界面中选择字典项目了,如下所示对于支付方式的字典项目,可以从中选择。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203143007577-1435242438.png" alt="image" width="624" height="173" loading="lazy"></p>
<p>而对于选择用户表、角色表、机构表,以及选择动态EAV生成的表,那么也需要进行一些属性值的关联复制处理,那么需要通过映射源字段和目标字段的名称,实现关联。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203144127997-974532534.png" alt="image" width="622" height="346" loading="lazy"></p>
<p> 这样就可以再选择产品信息的记录的时候,把它的属性值带到目标记录上。</p>
<p>如在订单明细记录中,通过产品选择的方式,可以带过来对应的属性值。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203144325895-143143890.png" alt="image" width="724" height="109" loading="lazy"></p>
<p> </p>
<h3>3、数据的查询和MongoDB </h3>
<p>使用EAV(Entity-Attribute-Value)模式来存储完整的数据结构信息以及NoSQL数据库来存储完整的记录是一种灵活的方法,特别适用于需要存储动态结构数据的场景。</p>
<p>EAV的常规关系型数据库表存储常规的设计表,如实体类型、属性定义、属性值(多个)表的相关信息,而利用MongoDB数据库的大数据处理灵活性和高性能的响应,能够存储我们实际变化的文档信息。在检索的时候,并提供了常规关系型数据库的联合查询、JSON查询无法得到的灵活性和高性能。 </p>
<p>有了字段的定义,我们就可以在业务列表中显示相关的字段,并从MongoDB总检索指定类型的数据,由于MongoDB本身支持非常好的查询处理,因此对于查询来说非常简单。</p>
<p>表的数据在MongoDB中存储的,如下界面所示。</p>
<p> <img src="https://img2024.cnblogs.com/blog/8867/202405/8867-20240514124000765-158907995.png" alt="" width="648" height="617" class="medium-zoom-image" loading="lazy"></p>
<p>对于在Python界面中展示相关的记录,我们根据配置,构建一个窗体界面,适配条件动态展示、列表展示、是否分页,以及对于各种属性定义好对应的前端输入类型,就能很好的实现数据的展示和直接录入的处理了。</p>
<p>由于我们前端后端通过WebAPI进行交互,后端在FastAPI服务中提供对应MongoDB的数据常规CRUD的接口来处理数据的增删改查操作,如下所示。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202512/8867-20251203144726288-583461297.png" alt="image" width="795" height="843" loading="lazy"></p>
<p> 对于条件查询的处理,这个和我们常规方式设计业务表,生成的查询接口类似,如下所示。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">@router.post(
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/mongo-list-post</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
response_model</span>=AjaxResponse |<span style="color: rgba(0, 0, 0, 1)"> None],
summary</span>=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">分页获取实体类型的记录</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
dependencies</span>=<span style="color: rgba(0, 0, 0, 1)">,
)
async </span><span style="color: rgba(0, 0, 255, 1)">def</span><span style="color: rgba(0, 0, 0, 1)"> mongo_get_list_post(
request: Request,
input: Annotated,
db: AsyncSession </span>=<span style="color: rgba(0, 0, 0, 1)"> Depends(get_db),
):
logger.info(f</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">EAVPagedDto:{input.model_dump()}</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
item </span>=<span style="color: rgba(0, 0, 0, 1)"> await attribute_crud.mongo_get_list(db, input)
</span><span style="color: rgba(0, 0, 255, 1)">return</span> AjaxResponse(item)</pre>
</div>
<p>而在Python的前端,我们这里以PySide6的界面实现为例,通过实体类型的定义和对应属性的列表信息,我们可以进行界面的动态构建,如下是PySide6的界面实现代码。</p>
<div class="cnblogs_code">
<pre> async <span style="color: rgba(0, 0, 255, 1)">def</span> _create_content_panel(self) -><span style="color: rgba(0, 0, 0, 1)"> QWidget:
</span><span style="color: rgba(128, 0, 0, 1)">"""</span><span style="color: rgba(128, 0, 0, 1)">创建右侧主要内容面板</span><span style="color: rgba(128, 0, 0, 1)">"""</span>
<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 创建一个主面板</span>
panel =<span style="color: rgba(0, 0, 0, 1)"> QWidget(self)
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 创建一个垂直布局</span>
main_layout =<span style="color: rgba(0, 0, 0, 1)"> QVBoxLayout()
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 创建一个折叠的查询条件框</span>
search_bar =<span style="color: rgba(0, 0, 0, 1)"> self._create_search_bar(panel)
main_layout.addWidget(search_bar)
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 创建显示数据的表格</span>
table_widget =<span style="color: rgba(0, 0, 0, 1)"> self._create_grid(panel)
main_layout.addWidget(table_widget, </span>1)<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 拉伸占用高度</span>
<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 如果是分页,创建一个分页控件</span>
<span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> self.ispaging:
self.pager_bar </span>=<span style="color: rgba(0, 0, 0, 1)"> ctrl.MyPager(panel, self.items_per_page, self.update_grid)
main_layout.addWidget(self.pager_bar)
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 创建从表数据表格</span>
sub_table_widget =<span style="color: rgba(0, 0, 0, 1)"> await self._create_sub_content(panel)
</span><span style="color: rgba(0, 0, 255, 1)">if</span> sub_table_widget <span style="color: rgba(0, 0, 255, 1)">is</span> <span style="color: rgba(0, 0, 255, 1)">not</span><span style="color: rgba(0, 0, 0, 1)"> None:
main_layout.addWidget(sub_table_widget, </span>1)<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 拉伸占用高度</span>
<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 设置布局</span>
<span style="color: rgba(0, 0, 0, 1)"> panel.setLayout(main_layout)
</span><span style="color: rgba(0, 0, 255, 1)">return</span> panel</pre>
</div>
<p>在查询条件的展示处理中,我们根据属性列表来统一构建显示的控件信息,如下代码所示。</p>
<div class="cnblogs_code">
<pre> <span style="color: rgba(0, 0, 255, 1)">def</span> CreateConditionsWithSizer(self, parent: QWidget = None) -><span style="color: rgba(0, 0, 0, 1)"> QGridLayout:
</span><span style="color: rgba(128, 0, 0, 1)">"""</span><span style="color: rgba(128, 0, 0, 1)">子类可重写该方法,创建折叠面板中的查询条件,包括布局 QGridLayout</span><span style="color: rgba(128, 0, 0, 1)">"""</span><span style="color: rgba(0, 0, 0, 1)">
layout </span>=<span style="color: rgba(0, 0, 0, 1)"> QGridLayout()
layout.setAlignment(Qt.AlignmentFlag.AlignLeft </span>|<span style="color: rgba(0, 0, 0, 1)"> Qt.AlignmentFlag.AlignVCenter)
layout.setSpacing(</span>2)<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 增加间距</span>
<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 统一处理查询条件控件的添加,使用默认的布局方式</span>
cols = self.CONDITIONS_PER_ROW * 2 <span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 每行显示的条件数,*2表示包含标签和输入控件</span>
self.condition_widgets = list =<span style="color: rgba(0, 0, 0, 1)"> EAVUtil.CreateConditions(self.attribute_list, parent)
</span><span style="color: rgba(0, 0, 255, 1)">for</span> i <span style="color: rgba(0, 0, 255, 1)">in</span><span style="color: rgba(0, 0, 0, 1)"> range(len(list)):
control </span>=<span style="color: rgba(0, 0, 0, 1)"> list
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> print(type(control))</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> <span style="color: rgba(0, 0, 255, 1)">not</span><span style="color: rgba(0, 0, 0, 1)"> isinstance(control, QWidget):
</span><span style="color: rgba(0, 0, 255, 1)">print</span>(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">错误!control 不是 QWidget:</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">, control)
</span><span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">
control.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed))</span>
layout.addWidget(control, i // cols, i %<span style="color: rgba(0, 0, 0, 1)"> cols)
</span><span style="color: rgba(0, 0, 255, 1)">return</span> layout</pre>
</div>
<p>而对于数据的直接编辑处理,我们通过控件属性的定义,统一转换为对应自定义视图委托的配置信息,从而构建不同的输入控件方式。</p>
<div class="cnblogs_code">
<pre> config =<span style="color: rgba(0, 0, 0, 1)"> EAVUtil.convert_attributes_to_config(self.attribute_list, self.entity_type_info)
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 设置QTableView视图的委托处理对象</span>
self.delegate =<span style="color: rgba(0, 0, 0, 1)"> CustomDelegate(config, self.table_model, self.table_view)
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)">对特殊字段进行自定义的处理,如弹出选择对话框等</span>
self.delegate.customTriggered.connect(self.on_custom_triggered)</pre>
</div>
<p>其中的config例子可能是其中下面的定义。</p>
<div class="cnblogs_code">
<pre> <span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)">实际字段测试</span>
config =<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">ProductName</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">text</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Color</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">combo</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">dict_type_name</span><span style="color: rgba(128, 0, 0, 1)">"</span>:<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">产品颜色</span><span style="color: rgba(128, 0, 0, 1)">"</span>}, <span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)">字典类型名称</span>
<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Size</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">int</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Style</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">combo</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">dict_type_name</span><span style="color: rgba(128, 0, 0, 1)">"</span>:<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">产品款式</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Height</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">double</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">min</span><span style="color: rgba(128, 0, 0, 1)">"</span>: 0, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">max</span><span style="color: rgba(128, 0, 0, 1)">"</span>: 1000, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">decimals</span><span style="color: rgba(128, 0, 0, 1)">"</span>: 2, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">format</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">{0} cm</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Width</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">double</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">min</span><span style="color: rgba(128, 0, 0, 1)">"</span>: 0, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">max</span><span style="color: rgba(128, 0, 0, 1)">"</span>: 1000, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">decimals</span><span style="color: rgba(128, 0, 0, 1)">"</span>: 2, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">format</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">{0} cm</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Note</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">text</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Test</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">text</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Tag</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">text</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">CreateDate</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">datetime</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">format</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">yyyy-MM-dd HH:mm:ss</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Price</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">double</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">decimals</span><span style="color: rgba(128, 0, 0, 1)">"</span>: 2,<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">format</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">{0:C2}</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Status</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">check</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">true</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">1</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">false</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">0</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Dealer</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">radio</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">dict_type_name</span><span style="color: rgba(128, 0, 0, 1)">"</span>:<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">送货区域</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">width</span><span style="color: rgba(128, 0, 0, 1)">"</span>: 280<span style="color: rgba(0, 0, 0, 1)">},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Rating</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">rating</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Tag</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">text</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">User</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">text</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">readonly</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">:True},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Organ</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">text</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">readonly</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">:True},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Role</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">text</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">readonly</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">:True},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Attach</span><span style="color: rgba(128, 0, 0, 1)">"</span>: {<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">type</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">text</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">readonly</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">:True},
}</span></pre>
</div>
<p>这需要我们根据实际的配置信息进行构建config即可。</p>
<p>我们为了支持直接录入多种控件类型,那么需要自定义视图委托。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">class</span> CustomDelegate(QStyledItemDelegate):</pre>
</div>
<p>它支持的类型有:</p>
<div>
<div> - text: 普通文本 (QLineEdit)</div>
<div> - int: 整数 (QSpinBox)</div>
<div> - double: 浮点数 (QDoubleSpinBox)</div>
<div> - date: 日期 (QDateEdit)</div>
<div> - combo: 下拉选择 (QComboBox)</div>
<div> - check: 复选框 (直接显示)</div>
<div> - radio: 单选按钮组 (QRadioButton)</div>
<div> - slider: 滑动条 (QSlider)</div>
<div> - multiline: 多行文本 (QTextEdit)</div>
<div> - password: 密码文本 (QLineEdit)</div>
<div> - percent: 百分比 (QDoubleSpinBox)</div>
<div> - currency: 货币 (QDoubleSpinBox)</div>
<div> - time: 时间 (QTimeEdit)</div>
<div> - datetime: 日期时间 (QDateTimeEdit)</div>
<div> - color: 颜色选择 (QPushButton)</div>
<div> - icon: 图标选择 (QPushButton)</div>
<div> - bitmap: 位图选择 (QPushButton)</div>
<div> - rating: 评分 (StarRating)</div>
<div> - tablenumber: 表号生成器 (QLineEdit + QPushButton)</div>
<div> - select_entity: 选择EAV自定义实体记录,以及字段复制映射</div>
<div> - select_system: 选择系统类型, 如:用户、角色、组织架构等,以及字段复制映射</div>
<div> - custom: 自定义不可编辑控件,同时触发 customTriggered 信号,传出单元格索引和字段名称</div>
</div>
<p>这个我曾经在文章《在PySide6/PyQt6的开发框架中,增加对表格多种格式录入的处理,以及主从表的数据显示和保存操作》有所介绍。</p>
<p>表格中多种格式录入的效果示例如下。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202510/8867-20251021214324515-79686897.png" alt="image" class="medium-zoom-image"></p>
<p> </p>
<p>以上就是对于如何在Python开发中实现无代码、纯配置的业务界面展示和常规数据操作的处理分析过程,其中设计到EAV相关基础表的设计,MongoDB数据的查询处理、FastAPI接口数据的封装、前端界面的设计和对于多种输入控件的支持等方面内容,通过整合这些,可以快速的、弹性的实现多种业务记录信息的存储和展示。</p>
<p>以上思路具体实现的过程,抛砖引玉,希望大家有所感悟,并分享一下自己的宝贵经验和思路。</p>
</div>
<div id="MySignature" role="contentinfo">
<div style="border-right-color: #cccccc; border-right-width: 1px; border-right-style: solid; padding-right: 5px; border-top-color: #cccccc; border-top-width: 1px; border-top-style: solid; padding-left: 4px; font-size: 13px; padding-bottom: 4px; border-left-color: #cccccc; border-left-width: 1px; border-left-style: solid; width: 98%; padding-top: 4px; border-bottom-color: #cccccc; border-bottom-width: 1px; border-bottom-style: solid; background-color: #eeeeee;">
<img src="http://www.cnblogs.com/Images/OutliningIndicators/None.gif" align="top" alt>
<span style="color: #000000"><span class="Apple-tab-span" style="white-space: pre"></span>
专注于代码生成工具、.Net/Python 框架架构及软件开发,以及各种Vue.js的前端技术应用。著有Winform开发框架/混合式开发框架、微信开发框架、Bootstrap开发框架、ABP开发框架、SqlSugar开发框架、Python开发框架等框架产品。
<br> 转载请注明出处:撰写人:伍华聪 http://www.iqidi.com <br> </span></div><br><br>
来源:https://www.cnblogs.com/wuhuacong/p/19301389
頁:
[1]