母版-布局-DSL 渲染基础(v4)

概述

不论是母版布局还是DSL,我们统一使用XML进行定义,可以更好的提供结构化表述。

参考文档:

下面文档中未介绍到的Mask母版Layout布局,可以去数据库中base库的表base_layout_definitionbase_mask_definitiontemplate字段查看

母版

确定了主题、非主内容分发区域所使用组件和主内容分发区域联动方式的页面配置。

母版内容分为主内容分发区域与非主内容分发区域。非主内容分发区域一般包含顶部栏、底部栏和侧边栏。侧边栏可以放置菜单,菜单与主内容分发区域内容进行联动。

image.png

默认母板

<mask>
    <multi-tabs />
    <header>
        <widget widget="app-switcher" />
        <block>
            <widget widget="notification" />
            <widget widget="divider" />
            <widget widget="language" />
            <widget widget="divider" />
            <widget widget="user" />
        </block>
    </header>
    <container>
        <sidebar>
            <widget widget="nav-menu" height="100%" />
        </sidebar>
        <content>
            <breadcrumb />
            <block width="100%">
                <widget width="100%" widget="main-view" />
            </block>
        </content>
    </container>
</mask>

该模板中包含了如下几个组件:

  • mask:母版根标签
  • multi-tabs:多选项卡
  • header:顶部栏
  • container:容器
  • sldebar:侧边栏
    • nav-menu:导航菜单
  • content:主内容
    • breadcrumb:面包屑
    • block:div块
    • main-view:主视图;用于渲染布局和DSL等相关内容;

母版将整个页面的大体框架进行了描述,接下来将主要介绍布局和DSL是如何在main-view中进行渲染的。关于自定义母版组件的相关内容 点击查看

布局

布局是将页面拆分成一个一个的小单元,按照从上到下、从左到右进行顺序排列

布局主要用于控制页面中元素的展示的相对位置,原则上不建议将元数据相关内容在布局中进行使用,可最大化布局的利用率。

默认表格视图(TABLE)

<view type="TABLE">
    <pack widget="group">
        <view type="SEARCH">
            <element widget="search" slot="search" />
        </view>
    </pack>
    <pack widget="group" slot="tableGroup">
        <element widget="actionBar" slot="actionBar">
            <xslot name="actions" />
        </element>
        <element widget="table" slot="table">
            <element widget="expandColumn" slot="expandRow" />
            <xslot name="fields" />
            <element widget="rowActions" slot="rowActions" />
        </element>
    </pack>
</view>

该模板中包含了如下几个组件:

  • view:视图;用于定义当前视图类型,不同的视图类型会有不同的数据交互,以及渲染不同的组件。
  • pack:容器类型相关组件。
  • element:元素组件;包含各种各样的组件,根据组件实现有不同的作用。
  • xslot:DSL插槽;用于将DSL中定义的模板分别插入到对应的槽中;

特别的,任何XML标签上的slot属性都具备DSL插槽的全部能力。当学习完DSL相关内容后,我们将会对DSL插槽有比较清晰的理解。

PS:在下面的内容中,将使用该布局进行描述。

DSL

准备工作

为了方便描述DSL和元数据之间的关系,我们需要先定义一个简单模型,这个模型里面包含字段和动作。这些通常是服务端定义的。(对服务端不感兴趣的同学可以跳过代码部分)

DemoModel.java
@Model.model(DemoModel.MODEL_MODEL)
@Model(displayName = "演示模型", labelFields = {"name"})
public class DemoModel extends IdModel {

    private static final long serialVersionUID = -7211802945795866154L;

    public static final String MODEL_MODEL = "demo.DemoModel";

    @Field(displayName = "名称")
    private String name;

    @Field(displayName = "是否启用")
    private Boolean isEnabled;
}
DemoModelAction.java
@Model.model(DemoModel.MODEL_MODEL)
@UxRouteButton(
        action = @UxAction(name = "redirectCreatePage", displayName = "创建", contextType = ActionContextTypeEnum.CONTEXT_FREE),
        value = @UxRoute(model = DemoModel.MODEL_MODEL, viewName = "演示模型form"))
@UxRouteButton(
        action = @UxAction(name = "redirectUpdatePage", displayName = "编辑", contextType = ActionContextTypeEnum.SINGLE),
        value = @UxRoute(model = DemoModel.MODEL_MODEL, viewName = "演示模型form"))
public class DemoModelAction {

    @Action(displayName = "启用")
    public DemoModel enable(DemoModel data) {
        data.setIsEnabled(true);
        data.updateById();
        return data;
    }

    @Action(displayName = "禁用")
    public DemoModel disable(DemoModel data) {
        data.setIsEnabled(false);
        data.updateById();
        return data;
    }
}

上面的java代码定义了演示模型字段动作

  • 字段 field
    • id:ID 整数 Integer
    • name:名称 字符串 String
    • isEnabled:是否启用 布尔 Boolean
  • 动作 action
    • redirectCreatePage:创建;目标视图为"演示模型form" 跳转动作 ViewAction
    • redirectUpdatePage:编辑;目标视图为"演示模型form" 跳转动作 ViewAction
    • enable:启用 提交动作 ServerAction
    • disable:禁用 提交动作 ServerAction

根据上面的元数据内容,我们可以定义一个如下所示的DSL模板,来使用所有的元数据将其渲染到页面中:

<view type="TABLE" title="演示表格" name="演示模型table" model="demo.DemoModel">
    <template slot="search">
        <field data="id" invisible="true" />
        <field data="name" />
        <field data="isEnabled" />
    </template>
    <template slot="actions">
        <action name="redirectCreatePage" />
    </template>
    <template slot="fields">
        <field data="id" invisible="true" />
        <field data="name" />
        <field data="isEnabled" />
    </template>
    <template slot="rowActions">
        <action name="redirectUpdatePage" />
        <action name="enable" />
        <action name="disable" />
    </template>
</view>

该模板中包含的标签含义:

  • view:视图;与布局模板中的view标签作用一致。
  • template:向DSL插槽中插入一个模板片段
  • field:字段元数据标签;data属性与元数据中字段API名称(field)对应。
  • action:动作元数据标签;name属性与元数据中动作API名称(name)对应。

DSL模板本质上是对布局模板的补充,它使用布局模板中已经定义好的DSL插槽,对其进行相应规则的合并替换,最终构成一个可被执行的视图模板再经过客户端渲染展示给用户。单独解释DSL模板的结构化含义是完全没有意义的。

接下来,我们会分别定义不同的DSL模板,并结合默认表格视图的布局模板进行详细说明。

模板编译

服务端模板编译基础

进入页面时,客户端将通过加载跳转动作(ViewActionLoad)接口获取对应的布局模板和DSL模板,得到经过服务端编译JSON数据,类似于如下数据结构:(DSL模板的JSON结构)

{
    "dslNodeType": "VIEW",
    "type": "TABLE",
    ......,
    "widgets": [
        {
            "dslNodeType": "TEMPLATE",
            "slot": "search",
            "widgets": [
                ......
            ]
        },
        {
            "dslNodeType": "TEMPLATE",
            "slot": "actions",
            "widgets": [
                ......
            ]
        }
    ]
}

服务端编译过程可以简单的理解为以下几个内容:(实际要复杂很多,在这里我们仅需要关注最终拿到的JSON结果即可)

  • XML转换为JSON结构
  • XML标签转换为dslNodeType属性,XML子标签转换为widgets属性,其他属性保持不变
  • field标签(字段)action标签(动作)对应的元数据补充完整。如上所示,我们仅定义了字段的data属性,返回的JSON中将包含字段动作的全部元数据。字段会补充字段类型、显示名称、是否存储等元数据属性,动作会补充动作类型、显示名称等元数据属性。

示例模板

表格视图-布局模板
<view type="TABLE">
    <pack widget="group">
        <view type="SEARCH">
            <element widget="search" slot="search" />
        </view>
    </pack>
    <pack widget="group" slot="tableGroup">
        <element widget="actionBar" slot="actionBar">
            <xslot name="actions" />
        </element>
        <element widget="table" slot="table">
            <element widget="expandColumn" slot="expandRow" />
            <xslot name="fields" />
            <element widget="rowActions" slot="rowActions" />
        </element>
    </pack>
</view>
表格视图-DSL模板
<view type="TABLE" title="演示表格" name="演示模型table" model="demo.DemoModel">
    <template slot="search">
        <field data="id" invisible="true" />
        <field data="name" />
        <field data="isEnabled" />
    </template>
    <template slot="actions">
        <action name="redirectCreatePage" />
    </template>
    <template slot="fields">
        <field data="id" invisible="true" />
        <field data="name" />
        <field data="isEnabled" />
    </template>
    <template slot="rowActions">
        <action name="redirectUpdatePage" />
        <action name="enable" />
        <action name="disable" />
    </template>
</view>

以上所示的两个模板(布局模板DSL模板)会作为布局和DSL的模板合并相关内容介绍的演示模板。更多的布局模板点击查看

布局和DSL的模板合并

当我们从服务端获取了布局模板DSL模板对应的JSON后,客户端将对这两个JSON进行合并处理。

合并操作主要是将DSL模板中定义的根标签下定义的所有template标签,向布局模板中定义的DSL插槽进行属性合并和子节点替换。

标准合并

将上述两个模板进行合并后,将得到如下结果:(为了方便观察结构,将使用XML结构进行解释,实际运行时为JSON结构)

<view type="TABLE">
    <pack widget="group">
        <view type="SEARCH">
            <element widget="search" slot="search">
                <!-- slot="search" -->
                <field data="id" invisible="true" />
                <field data="name" />
                <field data="isEnabled" />
            </element>
        </view>
    </pack>
    <pack widget="group" slot="tableGroup">
        <element widget="actionBar" slot="actionBar">
            <!-- slot="actions" -->
            <action name="redirectCreatePage" />
        </element>
        <element widget="table" slot="table">
            <element widget="expandColumn" slot="expandRow" />
            <!-- slot="fields" -->
            <field data="id" invisible="true" />
            <field data="name" />
            <field data="isEnabled" />
            <element widget="rowActions" slot="rowActions">
                <!-- slot="rowActions" -->
                <action name="redirectUpdatePage" />
                <action name="enable" />
                <action name="disable" />
            </element>
        </element>
    </pack>
</view>

由此可见,DSL模板定义的template标签下的内容被合并到了布局模板的对应位置中。接下来就可以进行模板渲染操作了。

子标签替换和属性合并

一般的,我们将处理布局模板中任何标签上的slot属性,以及xslot标签。

遵循就近原则,模板合并将取路径最短的模板进行替换和插入。

当我们需要设置表格所有字段都允许排序时,我们可以使用sortable="true"属性,设置在widget="table"组件上。

使用slot="fields"插槽是无法向widget="table"组件添加额外属性的,此时我们需要使用slot="table"这个插槽来实现我们的需求。

一个错误的DSL模板可能被定义成如下内容:(下面的示例舍弃了部分插槽内容)

<view type="TABLE" title="演示表格" name="演示模型table" model="demo.DemoModel">
    <template slot="table" sortable="true">
        <field data="id" invisible="true" />
        <field data="name" />
        <field data="isEnabled" />
    </template>
    <template slot="rowActions">
        <action name="redirectUpdatePage" />
        <action name="enable" />
        <action name="disable" />
    </template>
</view>

在使用相同的布局模板情况下,合并后的结果为:

<view type="TABLE">
    <pack widget="group">
        <view type="SEARCH">
            <element widget="search" slot="search" />
        </view>
    </pack>
    <pack widget="group" slot="tableGroup">
        <element widget="actionBar" slot="actionBar" />
        <element widget="table" slot="table" sortable="true">
            <!-- slot="table" -->
            <field data="id" invisible="true" />
            <field data="name" />
            <field data="isEnabled" />
        </element>
    </pack>
</view>

由此可见,我们优先使用了widget="table"上的slot="table"属性作为插槽,将原本定义在widget="table"下的所有节点全部换成了在DSL模板中定义的内容,并且成功的添加了sortable="true"这一属性。

但是,由于子标签被完全替换了,因此丢弃了widget="rowActions"组件,导致slot="rowActions"中定义的内容,找不到对应的插槽,这部分DSL模板内容将被丢弃。

反向合并

由于上面的合并操作丢弃了widget="rowActions"组件,但我们的本意并不是想要丢弃这个组件,而是仍然希望能以某种规则将这部分内容继续保留。为了解决这个问题,我们提供了反向合并操作,但前提是,DOM结构必须是同一层级

基于这一特性,我们将上述模板稍加修改:(将slot="rowActions"放在slot="table"下面)

<view type="TABLE" title="演示表格" name="演示模型table" model="demo.DemoModel">
    <template slot="table" sortable="true">
        <field data="id" invisible="true" />
        <field data="name" />
        <field data="isEnabled" />
        <template slot="rowActions">
            <action name="redirectUpdatePage" />
            <action name="enable" />
            <action name="disable" />
        </template>
    </template>
</view>

在使用相同的布局模板情况下,合并后的结果为:

<view type="TABLE">
    <pack widget="group">
        <view type="SEARCH">
            <element widget="search" slot="search" />
        </view>
    </pack>
    <pack widget="group" slot="tableGroup">
        <element widget="actionBar" slot="actionBar" />
        <element widget="table" slot="table" sortable="true">
            <!-- slot="table" -->
            <field data="id" invisible="true" />
            <field data="name" />
            <field data="isEnabled" />
            <element widget="rowActions" slot="rowActions">
                <!-- slot="rowActions" -->
                <action name="redirectUpdatePage" />
                <action name="enable" />
                <action name="disable" />
            </element>
        </element>
    </pack>
</view>

由此可见,原本定义在布局模板上的widget="rowActions"被找回来了,并且正确填充了定义在DSL模板中的内容。

不足的是,由于widget="table"下面的所有内容都被替换了一次,我们无法确定原本的widget="rowActions"组件应该如何正确放置,因此使用了DSL模板中定义的位置。

这可能会带来一些弊端,比如:将某一元素必须被放在第一个节点处。针对这种情况,布局模板本来是可以做到限定作用的,但由于使用了更大范围的插槽,使得这一特性丢失。

但也带来了一些灵活性,比如:大多数情况下,某一元素需要放在第一个节点处,但在特殊情况下,该元素需要放在最后一个节点处。

我们希望插槽可以设计的尽可能的灵活,以满足各种不同场景的需求。

属性插槽

当我们希望使用属性合并,但不希望替换子标签时,我们可以使用属性插槽来解决这个问题。

当我们希望向widget="group"组件上添加title="这是一个示例标题"属性时,我们可以使用slot="tableGroup"插槽。

我们可以使用如下所示的方式来定义DSL模板:

<view type="TABLE" title="演示表格" name="演示模型table" model="demo.DemoModel">
    <template slot="tableGroup" title="这是一个示例标题" />
    <template slot="table" sortable="true">
        <field data="id" invisible="true" />
        <field data="name" />
        <field data="isEnabled" />
        <template slot="rowActions">
            <action name="redirectUpdatePage" />
            <action name="enable" />
            <action name="disable" />
        </template>
    </template>
</view>

在使用相同的布局模板情况下,合并后的结果为:

<view type="TABLE">
    <pack widget="group">
        <view type="SEARCH">
            <element widget="search" slot="search" />
        </view>
    </pack>
    <pack widget="group" slot="tableGroup" title="这是一个示例标题">
        <element widget="actionBar" slot="actionBar" />
        <element widget="table" slot="table" sortable="true">
            <!-- slot="table" -->
            <field data="id" invisible="true" />
            <field data="name" />
            <field data="isEnabled" />
            <element widget="rowActions" slot="rowActions">
                <!-- slot="rowActions" -->
                <action name="redirectUpdatePage" />
                <action name="enable" />
                <action name="disable" />
            </element>
        </element>
    </pack>
</view>

由此可见,当DSL模板中的template标签下没有任何子标签时,我们仅会处理属性合并,但不会处理在原布局模板下定义的子标签。

根据这一特性,我们也可以定义这样的DSL模板来实现上面给widget="table"添加sortable="true"的需求:

<view type="TABLE" title="演示表格" name="演示模型table" model="demo.DemoModel">
    <template slot="tableGroup" title="这是一个示例标题" />
    <template slot="table" sortable="true" />
    <template slot="fields">
        <field data="id" invisible="true" />
        <field data="name" />
        <field data="isEnabled" />
    </template>
    <template slot="rowActions">
        <action name="redirectUpdatePage" />
        <action name="enable" />
        <action name="disable" />
    </template>
</view>

在使用相同的布局模板情况下,合并后的结果为:(与之前的合并结果完全一致)

<view type="TABLE">
    <pack widget="group">
        <view type="SEARCH">
            <element widget="search" slot="search" />
        </view>
    </pack>
    <pack widget="group" slot="tableGroup" title="这是一个示例标题">
        <element widget="actionBar" slot="actionBar" />
        <element widget="table" slot="table" sortable="true">
            <!-- slot="table" -->
            <field data="id" invisible="true" />
            <field data="name" />
            <field data="isEnabled" />
            <element widget="rowActions" slot="rowActions">
                <!-- slot="rowActions" -->
                <action name="redirectUpdatePage" />
                <action name="enable" />
                <action name="disable" />
            </element>
        </element>
    </pack>
</view>

由于属性插槽本身没有子标签,因此我们建议将所有的属性插槽配置在view标签子标签的最前面进行统一定义。

模板渲染

模板渲染是将一个包含dslNodeType属性的对象,生成对应框架的VDom对象,通过该VDom对象执行一段创建ts组件的逻辑,并通过ts组件渲染该组件对应的VDom对象

例如:使用Vue框架时,该VDom对象通常为VNode

(以下内容均围绕Vue框架进行描述,其他框架基本一致)

为了将模板渲染尽可能的接近第三方框架的渲染逻辑,我们采用了Tag(Vue组件) - Class Component(ts) - Component(Vue组件)这样的三层结构进行模板渲染的实现。

  • Tag(Vue组件):使用resolveComponent方法,将一个包含dslNodeType属性的对象,通过既定规则进行解析,获取一个Vue组件作为入口。主要处理模板标签和Vue组件之间的对应关系。
  • Class Component(ts):使用ts面向对象的特性,可以对组件逻辑进行重载、继承等处理,增加组件的灵活性。
  • Component(Vue组件):主要实现数据渲染。

具体的渲染逻辑不在此处进行介绍,该篇文章主要是为了让读者对模板以及整体框架有一个基本的概念,使用这套模板渲染的最终目的是为了让组件复用率更高,开发更加灵活。

Oinone社区 作者:oinone原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/23.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(1)
oinone的头像oinone
上一篇 2023年6月20日 pm4:07
下一篇 2023年11月2日 pm1:58

相关推荐

  • 自定义的「视图、字段」调用界面设计器配置的按钮(包含权限控制)

    我们在业务开发中,经常会遇到自定义的视图或字段。有时候期望点击某一块区域的时候,打开一个弹窗或者是跳转新页面亦或者是执行服务端动作(调接口),但是希望这个动作是界面设计器拖拽进来的。 这篇文章详细的讲解了自定义的视图、字段怎么执行界面设计器拖出来的按钮。 自定义视图 1: 先设计一个页面,把对应的动作拖拽进来,可以不需要配置字段2: 将该页面绑定菜单 3: 自定义对应的页面 当我们自定义视图的时候,首先会注册一个视图,下面是我自定义的一个表单视图 registerLayout( `<view type="FORM"> <element widget="actionBar" slot="actionBar"> <xslot name="actions" /> </element> <element widget="MyWidget" slot="form"> <xslot name="fields" /> </element> </view>`, { moduleName: 'ys0328', model: 'ys0328.k2.Model0000000453', actionName: 'MenuuiMenu78ec23b054314ff5a12b4fe95fe4d7b5', viewType: ViewType.Form } ); 我自定义了一个叫做MyWidget的 element,下面是对应的ts代码 @SPI.ClassFactory(BaseElementWidget.Token({ widget: 'MyWidget' })) export class MyWidgetManageWidget extends FormWidget { public initialize(props): this { super.initialize(props); this.setComponent(MyVue); return this; } } 这是对应的 vue 文件: MyVue.vue <template> <div @click="onClick">点击执行动作</div> </template> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({ props: ['onClick'] }); </script> 这个时候,我希望点击的时候,执行 onClick,会执行对应的动作,这时只需要在对应的 ts 文件中写对应的代码逻辑即可: @SPI.ClassFactory(BaseElementWidget.Token({ widget: 'MyWidget' })) export class MyWidgetManageWidget extends BaseElementWidget { // 获取当前页面所有的按钮 @Widget.Reactive() public get modelActions() { return this.metadataRuntimeContext.model.modelActions || [] } // 用来解析上下文表达式的,如果不需要,可以删除 public executeCustomExpression<T>( parameters: Partial<ExpressionRunParam>, expression: string, errorValue?: T ): T | string | undefined { const scene = this.scene; return Expression.run( { activeRecords: parameters.activeRecords, rootRecord: parameters.rootRecord, openerRecord: parameters.openerRecord, scene: parameters.scene || scene, activeRecord: parameters.activeRecord } as ExpressionRunParam, expression, errorValue ); } // 点击事件 @Widget.Method() public onClick() { // 找到对应的按钮 const action = this.modelActions.find((a) => a.label === '动作的显示名称'); /** * 如果是服务端动作,就执行 executeServerAction */ // executeServerAction(action, 参数对象) // 第二个参数是调用对应的接口传递的参数 /** *…

    2023年11月8日
    1.2K02
  • 弹窗生命周期实践

    在oinone平台中,弹窗、抽屉是用户界面设计中最为常见的,而对于开发者而言,能够监听弹窗的生命周期事件通常是十分重要的,因为它提供了一个机会去执行一些逻辑。在这篇文章中,我们将深入探讨如何监听弹窗、抽屉生命周期事件,并讨论一些可能的应用场景。 首先,我们来实现一个监听弹窗销毁的事件。 让我们看一下提供的代码片段: // 1: 自定义打开弹窗的动作 @SPI.ClassFactory( BaseActionWidget.Token({ actionType: [ActionType.View], target: [ViewActionTarget.Dialog], model: 'model', name: 'name' }) ) export class MyDialogViewActionWidget extends DialogViewActionWidget { protected subscribePopupDispose = (manager: IPopupManager, instance: IPopupInstance, action) => { // 自定义销毁弹窗后的逻辑 }; protected mounted() { PopupManager.INSTANCE.onDispose(this.subscribePopupDispose.bind(this)); } protected unmounted() { PopupManager.INSTANCE.clearOnDispose(this.subscribePopupDispose.bind(this)); } } 在上面的代码中,我们自定义了打开弹窗的动作,并且监听了弹窗销毁事件。 让我们逐步解析这段代码: 1: subscribePopupDispose 是一个函数,作为弹窗销毁事件的处理程序。它接收三个参数:manager、instance 和 action。 manager: 弹窗事件管理器 instance: 弹窗实例 action: 操作弹窗的动作,如果是点击弹窗右上角的关闭按钮,那action为null 2: 组件挂载的时候,进行监听. 4: 最后组件销毁的时候需要清除对应的监听 那么,如果监听到弹窗销毁,我们可以执行什么样的逻辑呢? 1: 更新相关组件状态: 弹窗销毁后,可能需要更新其他组件的状态。通过 popupWidget 可以获取到弹窗相关的信息,进而执行一些状态更新操作。 2: 处理弹窗销毁时的数据或动作: 在 subscribePopupDispose 函数中,action 参数含一些关于弹窗销毁时的动作信息,如果是点击弹窗右上角的销毁按钮,那action为null。我们可以根据这些信息执行相应的逻辑,例如更新界面状态、保存用户输入等 3: 触发其他操作: 弹窗销毁后,可能需要触发一些后续操作,比如显示另一个弹窗、发起网络请求等。 完整的生命周期 方法名 功能描述 onPush(fn) 监听 弹出窗口被推入时的事件处理器 clearOnPush(fn) 清除onPush事件的监听 onCreated(fn) 监听 弹出窗口创建时的事件处理器 clearOnCreated(fn) 清除onCreated事件的监听 onOpen(fn) 监听 弹出窗口打开时的事件处理器 clearOnOpen(fn) 清除onOpen事件的监听 onClose(fn) 监听 弹出窗口关闭时的事件处理器 clearOnClose(fn) 清除onClose事件的监听 onDispose(fn) 监听 弹出窗口被销毁时的事件处理器 clearOnDispose(fn) 清除onDispose事件的监听 onDisposeAll(fn) 监听 所有弹出窗口被销毁时的事件处理器 clearOnDisposeAll(fn) 清除onDisposeAll事件的监听 结语 开发者可以更灵活地响应用户操作,提升用户体验。在实际项目中,根据应用需求和设计,可以根据以上优化逻辑定制具体的处理流程。希望这篇文章为你提供了更深入的理解。

    2023年11月17日
    97600
  • 前端表格复制

    我们可能会遇到表格复制的需求,也就是表格填写的时候,不是增加一行数据,而是增加一个表格。以下是代码实现和原理分析。 代码实现 在 boot 工程的 main.ts 中加入以下代码 import { registerDesignerFieldWidgetCreator, selectorDesignerFieldWidgetCreator } from '@oinone/kunlun-ui-designer-dependencies'; // 注册无代码组件,将表头分组的无代码组件,注册成字段表格组件 registerDesignerFieldWidgetCreator( { widget: 'DynamicCreateTable' }, selectorDesignerFieldWidgetCreator({ widget: TABLE_WIDGET })! ); DynamicCreateTableWidget 动态添加表格 ts 组件 import { FormO2MTableFieldWidget, Widget, DslDefinition, RuntimeView, SubmitValue, BaseFieldWidget, ModelFieldType, SPI, ViewType, ActiveRecord, uniqueKeyGenerator } from '@oinone/kunlun-dependencies'; import { MyMetadataViewWidget } from './MyMetadataViewWidget'; import DynamicCreateTable from './DynamicCreateTable.vue'; @SPI.ClassFactory( BaseFieldWidget.Token({ viewType: ViewType.Form, ttype: ModelFieldType.OneToMany, widget: 'DynamicCreateTable' }) ) export class DynamicCreateTableWidget extends FormO2MTableFieldWidget { public myMetadataViewWidget: MyMetadataViewWidget[] = []; @Widget.Reactive() public myMetadataViewWidgetLength = 0; @Widget.Reactive() public myMetadataViewWidgetKeys: string[] = []; protected props: Record<string, unknown> = {}; public initialize(props) { super.initialize(props); this.props = props; this.setComponent(DynamicCreateTable); return this; } // region 创建动态表格 @Widget.Method() public async createTableWidget(record: ActiveRecord) { const index = this.myMetadataViewWidget.length; const handle = uniqueKeyGenerator(); const slotKey = `MyMetadataViewWidget_${handle}`; const widget = this.createWidget( new MyMetadataViewWidget(handle), slotKey, // 插槽名称 { subIndex: index, metadataHandle: this.metadataHandle, rootHandle: this.rootHandle, automatic: true, internal: true, inline: true } ); this.initDynamicSubview(this.props, widget); widget.setData(record); this.myMetadataViewWidgetLength++; this.myMetadataViewWidgetKeys.push(slotKey); this.myMetadataViewWidget.push(widget); } protected initDynamicSubview(props: Record<string, unknown>, widget: MyMetadataViewWidget) { const { currentViewDsl } = this; let viewDsl = currentViewDsl; if (!viewDsl) { viewDsl = this.getViewDsl(props)…

    2025年7月21日
    61300
  • 【前端】生产环境部署及性能调优

    概述 前端工程使用vue-cli-service进行构建,生成dist静态资源目录,其中包括html、css、javascript以及其他项目中使用到的所有资源。 在生产环境中,我们通常使用Nginx开启访问服务,并定位其访问目录至dist目录下的index.html,以此来实现前端工程的访问。 不仅如此,为了使得前端发起请求时,可以正确访问到后端服务,也需要在nginx中配置相应的代理,使得访问过程在同域中进行,以达到Cookie共享的目的。 当然,服务部署的形式可以有多种,上述的部署方式也是较为常用的部署方式。 部署 使用production模式进行打包 使用dotenv-webpack插件启用process.env 配置chainWebpack调整资源加载顺序 使用thread-loader进行打包加速 性能调优 使用compression-webpack-plugin插件进行压缩打包 启用Nginx的gzip和gzip_static功能 使用OSS加速静态资源访问(可选) 使用production模式进行打包 在package.json中添加执行脚本 { "scripts": { "build": "vue-cli-service build –mode production" } } 执行打包命令 npm run build 使用dotenv-webpack插件启用process.env 参考资料 dotenv-webpack 在package.json中添加依赖或使用npm安装 { "devDependencies": { "dotenv-webpack": "1.7.0" } } npm install dotenv-webpack@1.7.0 –save-dev 在vue.config.js中添加配置 const Dotenv = require('dotenv-webpack'); module.exports = { publicPath: '/', productionSourceMap: false, lintOnSave: false, configureWebpack: { plugins: [ new Dotenv() ] } }; .env加载顺序 使用不同模式,加载的文件不同。文件按照从上到下依次加载。 development .env .env.development production .env .env.production 配置chainWebpack调整资源加载顺序 chainWebpack对资源加载顺序取决于name属性,而不是priority属性。如示例中的加载顺序为:chunk-a –> chunk-b –> chunk-c。 code>test属性决定其打包范围,但集合之间会自动去重。如chunk-a打包node_modules下所有内容,chunk-b打包node_modules/@kunlun下所有内容。因此在chunk-a中将不再包含node_modules/@kunlun中的内容。没有

    2024年4月19日
    1.1K00
  • 【界面设计器】自定义字段组件基础

    阅读之前 本文档属于高阶实战文档,已假定你了解了所有必读文档中的全部内容,并了解过界面设计器的一些基本操作。 如果在阅读过程中出现的部分概念无法理解,请自行学习相关内容。【前端】文章目录 概述 平台提供的字段组件(以下简称组件)是通过SPI机制进行查找并最终渲染在页面中。虽然平台内置了众多组件,但无法避免的是,对于业务场景复杂多变的实际情况下,我们无法完全提供所有组件。 面对这样的困境,我们提供了外部注册组件的方式。在之前文章中,我们了解到组件注册后需要应用到页面中,需要配合DSL才能实现。其实,在平台中还提供了一种SAAS化的组件注册方式,配合界面设计器的设计能力,可以将组件在设计器页面中引入,从而更近一步的满足不同的业务场景。 通过界面设计器可以创建自定义组件,并为组件添加对应的元件。 界面设计器可以为元件设计其在指定视图下的属性面板,在页面设计时,可以使用该属性面板为元件设置相关属性。 在界面设计器的设计页面中拖入的组件,将通过SPI机制获取到一个唯一的元件,并渲染在页面中,提供给业务使用。 界面设计器-组件管理 名词解释 页面设计:使用界面设计器设计页面的页面。 属性面板设计:使用界面设计器设计属性面板的页面。 设计页面:页面设计和属性面板设计的统称。 组件库:展示在设计页面左侧的全部可拖拽组件。 组件:在组件库中可拖拽的最小单元。 元件:一个组件中的具体实现,是组件的最小单元。 属性面板:展示在设计页面右侧的元件属性。 页面设计 属性面板设计 组件管理入口 进入界面设计器后,可通过上方标签页切换至【组件】管理页面。 创建第一个组件 点击【添加组件】,在弹窗中输入组件名称【文本输入框】后,点击【确定】。 创建第一个元件 点击【组件卡片】或点击【管理元件】按钮进入【元件】管理页面。 点击【添加元件】,可以看到如下【创建元件】弹窗。 表单字段解释 元件名称:显示名称,仅在管理页面做展示使用。 API名称:SPI中的widget属性。 支持字段业务类型:SPI中的ttype属性。 支持多值:SPI中的multi属性。 支持视图类型:SPI中的viewType属性。 元件描述:元件功能描述内容,仅在管理页面做展示使用。 填入以下内容,并点击【确定】。 设计元件属性面板 点击【元件卡片】或点击【设计元件属性】按钮进入【属性面板设计】页面。 从【模型】中搜索【标题】,将【标题】和【隐藏标题】拖放至设计区域。如果想实现的相对美观,可以额外添加【分组】组件拖放至设计区域,并修改标题为【基础】,如下图所示。 点击【发布】按钮进行页面的发布。 至此,我们设计了第一个元件属性面板,接下来,我们需要在页面设计中使用这个组件。 在页面中使用【文本输入框】 由于我们之前选择的支持的视图类型是【表单】,因此我们在【表单】页面进行接下来的操作,此处略去创建视图的过程。 从【模型】中将【名称】拖放至设计区域。并通过点击【切换】按钮切换至我们的组件【文本输入框】,并且将标题改为【这是文本输入框组件】查看其展示效果。 属性变化 在组件切换后,属性面板发生了变化,原有属性会根据当前属性面板中现有字段进行【裁剪】,相同属性名称(字段)的值会被保留,其他属性值会被丢弃。 由于我们并没有在当前属性面板添加【宽度】属性,因此原有属性的宽度被丢弃,组件会自动变成默认【宽度】,默认宽度为1。 组件变化 由于我们并没有在【低无一体】中上传对应元件的代码实现,因此展示了默认的【单行文本】组件,目前组件的展示效果不会发生变化。 组件可切换规则 只有【组件】中包含与【当前选中字段】匹配的【元件】,才会将对应【组件】的名称展示在【可切换列表】中。 【当前选中字段】中包含了如下三个属性,这三个属性和【创建元件】时设置的属性一一对应。 (右侧属性面板切换至字段后,可查看当前选中字段的相关元数据信息。) 所在视图类型:根据字段在视图中的位置进行推断,当前所在位置为【表单】。当【支持视图类型】包含【表单】时条件成立。 字段业务类型:文本。当【支持字段业务类型】包含【文本】时条件成立。 是否多值:否。当【支持多值】相同时条件成立。 使用低无一体为组件上传代码实现 进入【组件】管理页面,点击【低无一体】,打开【低无一体】弹窗。 按照步骤,在【生成SDK】后,可以【下载模板工程】。在本地进行npm相关操作后,会在packages/kunlun-plugin目录下生成dist目录。在dist目录中,会有对应的kunlun-sdk.umd.js文件,使用【上传JS文件】进行上传。如果工程中包含了css,使用【上传CSS文件】进行上传。上传完成后点击【确定】进行保存。 PS:在模板工程中,我们提供了最简化的Hello World示例,即使不添加任何代码也可以看到组件的具体效果,为了方便演示,我们暂时不介绍代码实现的相关内容,仅需直接上传对应js文件,看到效果即可。如果遇到相关问题,请点击查看【前端】低无一体部署常见问题。 结语 至此,我们已经完整体验了从【创建组件】到【属性面板设计】再到【使用组件】以及【实现组件】的全部流程。 通过这一流程我们不难发现,【自定义组件】并非仅仅用于【页面设计】,在【属性面板设计】时,我们同样可以使用【自定义组件】来设计【自定义组件】的属性面板。这样便形成了一个完整的设计闭环,使得开发者可以更大程度的发挥自身创造力,开发出符合业务需求的【自定义组件】。

    2023年11月1日
    2.2K00

Leave a Reply

登录后才能评论