前端视图的元数据与数据的传递、交互

在阅读本篇文章之前,您需要学习以下知识点:

1: 元数据

视图的元数据

在日常开发中,我们会经常遇到自定义的字段、动作、视图需要界面设计器配置的数据,这些数据可能是当前页面的字段,也有可能动作,那么如何获取呢?

视图元数据分为两种:
1: 当前视图(metadataRuntimeContext)
2: 根视图(rootRuntimeContext)

那么这两种类型怎么区分呢?

举个例子:
1: 如果当前字段是在表单中,那么当前视图就是表单,根视图就表单的父级视图,如果只有一个表单视图,那么当前视图就是根视图。
2: 如果当前视图是表单,但是表单里面有个表格,对于表格字段而言,当前视图就是表格,根视图就是表单。

当前视图的元数据(metadataRuntimeContext)

在前端,我们通过 metadataRuntimeContext 来获取视图的元数据,例如:

export class CustomFormStringFieldSingleWidget extends FormStringFieldSingleWidget {
  protected mounted(): void {
    console.log(this.metadataRuntimeContext);
  }

    /**
  * 界面设计器配置的动作
  */
  @Widget.Reactive()
  protected get modelActions() {
    return this.metadataRuntimeContext.model.modelActions
  }

   /**
  * 界面设计器配置的字段
  */
  @Widget.Reactive()
  protected get modelFields() {
    return this.metadataRuntimeContext.model.modelFields
  }
}
属性名 类型 可选性 描述
viewAction RuntimeViewAction 运行时跳转动作(通过跳转动作创建的运行时上下文具备该属性)
module RuntimeModule 运行时模块
model RuntimeModel 运行时模型
view RuntimeView 运行时视图
viewLayout DslDefinition \| undefined 视图布局 DSL,从运行时视图解析获得
viewDsl DslDefinition \| undefined 视图模板 DSL,从运行时视图解析获得
viewTemplate DslDefinition 视图最终执行 DSL,从运行时视图解析获得或根据布局 DSL 和模板 DSL 合并生成
getModel (model: string, isBelong?: boolean) => GetModelResult \| undefined 获取模型,返回获取的模型和所在的运行时上下文
getModelField (data: string, isBelong?: boolean) => GetModelFieldResult \| undefined 获取模型字段,返回获取的模型字段和所在的运行时上下文
getRequestModelFields (options?: GetRequestModelFieldsOptions) => RequestModelField[] 获取请求字段
getDefaultValue () => Promise<Record<string, unknown>> 获取默认值
getInitialValue () => Promise<Record<string, unknown>> 获取初始值

运行时模型(model)

属性名 类型 可选性 描述
id string 模型 id
model string 模型编码
name string 技术名称
modelFields RuntimeModelField[] 模型字段
modelActions RuntimeAction[] 模型动作
type ModelType 模型类型
module string 模块编码
moduleName string 模块名称
moduleDefinition RuntimeModule 模块定义
pks string[] 主键
uniques string[][] 唯一键
indexes string[][] 索引
sorting string 排序
label string 显示标题
labelFields string[] 标题字段

可通过modelFieldsmodelActions获取界面设计器配置的模型字段、模型动作。

当前视图的 dsl(viewDsl)

视图的 DSL,从运行时视图解析获得,如果模型中的modelFieldsmodelActions获取不到的对应的字段、动作,那么可从该属性中获取

默认值(getDefaultValue)

用来获字段的默认值(界面配置器配置的)

根视图的元数据(rootRuntimeContext)

根视图的元数据结构跟当前视图的元数据类似。

视图数据

视图数据是贯穿于应用程序前端和视图层的关键数据,它承载了应用逻辑到用户界面之间的桥梁作用。

在 oinone 前端中,常见的视图数据分为如下几种:

1: 表单数据(formData)
2: 列表数据(dataSource)
3: 根视图数据(rootData)
4: 当前字段、动作所在的区域数据(activeRecords)
5: 按钮打开弹窗、抽屉的时候,按钮所在的区域数据(openerActiveRecords)

formData

formData 是一个对象,对象中的每个属性对应一个表单项,表单项的值就是该属性的值。所有的表单字段都可以通过this.formData来访问,也可以通过this.formData.xxx来获取某个字段的值。

dataSource

dataSource 是一个数组,数组中的每个元素对应一个列表项,列表项的值就是该元素。所有的列表字段都可以通过this.dataSource来访问,也可以通过this.dataSource[0].xxx来获取某个列表字段的值。

rootData

rootData 是一个数组,如果当前视图是列表,那么 rootData 是列表的数据源。如果当前视图是表单,那么 rootData 是表单的数据源,其中的第 0 项是当前表单的数据。

activeRecords

activeRecords 是一个数组

1: 表单视图的动作
    1.1: activeRecords 是表单的数据源,其中的第 0 项是当前表单的数据。
2: 表格视图上方的动作
    2.1: activeRecords 是当前选中的行数据,其中的第 0 项是对应的数据。
3: 表格的行内动作
    3.1: activeRecords 是当前行数据,其中的第 0 项是对应的数据。

openerActiveRecords

openerActiveRecords 是一个数组,用于保存当前页面的 opener 页面选中的数据。

1: 比如 行内动作 点击打开弹窗,弹窗里面的字段或者动作可以通过 openerActiveRecords 获取 行内 数据.
2: 比如 表单中的动作 点击打开弹窗,弹窗里面的字段或者动作可以通过 openerActiveRecords 表单 数据.

特殊数据

activeTreeContext 树组件的选中数据

activeTreeContext 树组件的选中数据,可以获取到选中的数据,以及选中的数据的父级数据,用于左树右表的视图.

常见场景:

场景 1: 左树右表的视图,表格上方的创建按钮点击打开弹窗弹窗里面要获取左树选中的数据,用于创建数据.
方案:点击弹窗里面的创建按钮时,通过 this.metadataRuntimeContext.view?.activeTreeContext 获取到左树选中的数据

class CustomActon extends ServerActionWidget {
  protected async executeAction(action: RuntimeServerAction, submitValue: SubmitValue){
    const treeContext = this.metadataRuntimeContext.view?.activeTreeContext || {}
    submitValue.records = {
      ...submitValue.records,
      ...treeContext
    }
    super.executeAction(action, submitValue)
  }
}

场景 2: 左树右表的视图,表格上方的创建按钮点击打开新页面新页面里面要获取左树选中的数据,用于创建数据.
方案:点击新页面里面的创建按钮时,可通过URL参数获取到左树选中的数据

class CustomActon extends ServerActionWidget {
  protected async executeAction(action: RuntimeServerAction, submitValue: SubmitValue){
    const treeContext = JSON.parse(this.getUrlParameters().context).activeTreeContext || {}
    submitValue.records = {
      ...submitValue.records,
      ...treeContext
    }
    super.executeAction(action, submitValue)
  }
}

searchBody 搜索条件、searchConditions 固定查询条件

参考文章

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

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

Like (0)
汤乾华's avatar汤乾华数式员工
Previous 2024年10月8日 am10:37
Next 2024年10月8日 pm5:55

相关推荐

  • 自定义字段组件如何处理vue组件内的表单校验

    介绍 本示例以字符串字段为业务场景,将输入框用element-plus的组件实现了一遍,vue组件内在onMounted生命周期内将ElForm表单实例通过ts组件内提供到props的setFormInstance方法设置到了ts组件的属性formInstance上,这样就可以在ts组件校验方法validator()触发的时候直接调用表单组件实例formInstance的校验方法validate() 适用场景 当前字段存储了动态表单的配置json,vue组件内自行实现了一套表单渲染逻辑,需要在vue组件和ts组件内同时触发校验 参考文档 element-plus表单组件文档 如何编写自定义字段组件的校验逻辑 示例代码 ts组件 import { BaseFieldWidget, FormStringFieldSingleWidget, isValidatorSuccess, ModelFieldType, SPI, ValidatorInfo, ViewType, Widget } from '@kunlun/dependencies'; import { FormInstance } from 'element-plus'; import MyFormStringField from './MyFormStringField.vue'; @SPI.ClassFactory( BaseFieldWidget.Token({ viewType: [ViewType.Form, ViewType.Search], ttype: ModelFieldType.String, widget: 'Input', model: 'resource.k2.Model0000000109', name: 'code', }) ) export class MyFormStringFieldWidget extends FormStringFieldSingleWidget { public initialize(props) { super.initialize(props); this.setComponent(MyFormStringField); return this; } /** * ElementPlus的表单vue组件实例 * @private */ private formInstance?: FormInstance; @Widget.Method() private setFormInstance(formInstance: FormInstance | undefined) { this.formInstance = formInstance; } /** * 字段校验方法 */ public async validator(): Promise<ValidatorInfo> { const validRes = await this.formInstance?.validate((valid, fields) => {}); console.log('validRes', validRes) if (!validRes) { return this.validatorError('校验失败'); } const res = await super.validator(); if (!isValidatorSuccess(res)) { return res; } if (this.value == null) { return this.validatorSuccess(); } return this.validateLength(this.value); } } vue组件 <template> <ElForm ref="formInstance" :model="model" :rules="rules"> <ElFormItem label="编码" prop="code"> <ElInput v-model="model.code" @input="onValueChange"></ElInput> </ElFormItem> </ElForm> </template> <script lang="ts"> import { defineComponent, reactive, ref, onMounted, watch } from 'vue'; import { ElForm, ElFormItem, ElInput, FormInstance } from 'element-plus'; export default defineComponent({ name: 'MyFormStringField', components: { ElForm, ElFormItem, ElInput }, props: ['value', 'setFormInstance', 'onChange'],…

    2024年9月6日
    2.1K00
  • Oinone移动端快速入门

    介绍 oinone的pc端的页面默认都可以在移动端直接访问。自定义mask、layout、视图组件、字段组件、动作组件方式都参考pc端实现。目前移动端的UI组件是基于vant@3.6.0版本开发,如有自定义部分的代码,推荐使用该组件库。 “注意”: 由于移动端和pc端在交互上的巨大差异,两端用的是不同的UI组件库是,按照此约定开发的自定义组件在两端也是无法相互兼容的,在pc端自定义的组件或者页面,不会在移动端自动适配,需要自行开发对应的移动端组件或者页面。 工程搭建 移动端很多交互跟pc端差异很大,所以移动端的我们采用的方案是独立用一套移动端的UI框架实现,而不是简单的做页面布局自适应,所以移动端需要跟pc端一样独立部署一套前端工程。 参考文档:【前端】移动端工程结构最佳实践(v4/v5) 如何区分多端 在界面设计器设计页面的时候,可以通过顶部的多端设备的图标切换在各端的页面效果。 pc端页面 切换为移动端后的页面 注意:大部分情况下,pc端和移动端可以共享一个设计的页面,然后通过上面的方法区分两端,如果移动端的页面交互差异很大,那更推荐独立新建一个页面专门给移动端使用。 模块 模块在定义的时候可以通过注解@Module.clientTypes决定当前模块在哪些端展示 package pro.shushi.pamirs.demo.api; import org.springframework.stereotype.Component; import pro.shushi.pamirs.business.api.BusinessModule; import pro.shushi.pamirs.core.common.CommonModule; import pro.shushi.pamirs.file.api.FileModule; import pro.shushi.pamirs.meta.annotation.Module; import pro.shushi.pamirs.meta.base.PamirsModule; import pro.shushi.pamirs.meta.common.constants.ModuleConstants; import pro.shushi.pamirs.meta.enmu.ClientTypeEnum; import pro.shushi.pamirs.user.api.UserModule; @Component @Module( name = DemoModule.MODULE_NAME, displayName = "oinoneDemo工程", version = "1.0.0", // 客户端类型,默认是PC和MOBILE端都展示 clientTypes = {ClientTypeEnum.PC, ClientTypeEnum.MOBILE}, // 登录后默认访问 priority 值最小的模块 priority = 1, dependencies = {ModuleConstants.MODULE_BASE, CommonModule.MODULE_MODULE, UserModule.MODULE_MODULE, BusinessModule.MODULE_MODULE, FileModule.MODULE_MODULE } ) @Module.module(DemoModule.MODULE_MODULE) @Module.Advanced(selfBuilt = true, application = true) public class DemoModule implements PamirsModule { public static final String MODULE_MODULE = "demo_core"; public static final String MODULE_NAME = "DemoCore"; @Override public String[] packagePrefix() { return new String[]{ "pro.shushi.pamirs.demo" }; } } 菜单 界面设计器设置方式 在菜单设置的时候可以选择“显示设备” 低代码设置方式 通过注解@UxMenu.clientTypes设置显示设备 package pro.shushi.pamirs.demo.core.init.menu; import pro.shushi.pamirs.boot.base.constants.ViewActionConstants; import pro.shushi.pamirs.boot.base.ux.annotation.action.UxRoute; import pro.shushi.pamirs.boot.base.ux.annotation.navigator.UxMenu; import pro.shushi.pamirs.boot.base.ux.annotation.navigator.UxMenus; import pro.shushi.pamirs.demo.api.model.DemoItem; import pro.shushi.pamirs.demo.api.model.DemoItemCategory; import pro.shushi.pamirs.meta.enmu.ClientTypeEnum; @UxMenus() public class DemoMenus implements ViewActionConstants { // 同时在pc端和移动端显示 @UxMenu(value = "商品中心", clientTypes = {ClientTypeEnum.PC, ClientTypeEnum.MOBILE}) class ItemPMenu{ @UxMenu("商品类目") class DemoItemAndCateMenu { // 只在pc端显示 @UxMenu(value = "商品管理", clientTypes = {ClientTypeEnum.PC}) @UxRoute(DemoItem.MODEL_MODEL) class DemoItemMenu { } // 只在移动端显示 @UxMenu(value = "类目管理", clientTypes = {ClientTypeEnum.MOBILE}) @UxRoute(DemoItemCategory.MODEL_MODEL) class DemoItemCategoryMenu { } } } } 组件 界面设计器选中组件后,可以在右边属性面板看到“显示设备”的配置,默认为空,为空则表示在pc端和移动端都显示

    2024年9月19日
    2.3K00
  • oio-spin 加载中

    用于页面和区块的加载中状态。 何时使用 页面局部处于等待异步数据或正在渲染过程时,合适的加载动效会有效缓解用户的焦虑。 API 参数 说明 类型 默认值 版本 delay 延迟显示加载效果的时间(防止闪烁) number (毫秒) – loading 是否为加载中状态 boolean true wrapperClassName 包装器的类属性 string –

    2023年12月18日
    1.4K00
  • 如何实现页面间的跳转

    介绍 在日常的业务中,我们经常需要在多个模型的页面之间跳转,例如从商品的行可以直接点击链接跳转到类目详情,还有查看该订单发起的售后单列表,这里将给大家展示如何在oinone中如何实现这些功能。 方法一、通过界面设计器的无代码能力配置 表格行跳转到表单页/详情页 拖入一个跳转动作到表格行,保存动作后,在左侧的动作属性面板底部有个请求配置,里面的上下文属性就是配置跳转参数的地方,点击添加按钮可以增加一行参数 点击添加按钮后,可以看到新增了一行,行内有2个输入框,左侧输入框为目标视图模型的字段,右侧输入框为当前视图模型的表达式 注意 表达式中activeRecord关键字代表当前行的数据对象 “上下文”相关知识点 当前页面的模型和跳转后的页面模型相同的情况下,会字段带上当前行数据的id作为路由参数 上下文是从当前页面跳转到下个页面带的自定义参数 上下文会作为跳转后的页面数据加载函数的入参,后端的该函数需要根据该条件查询到数据返回给前端,典型的例子就是编辑页,根据id查询对象的其他字段信息返回 跳转后页面的数据加载函数可以在动作场景的时候选择加载函数,也可以在页面的加载函数处设置 方法二、通过低代码方式在自定义代码中调用 oinone提供了内置函数executeViewAction实现该功能 import { DefaultComparisonOperator, executeViewAction, QueryExpression, RuntimeViewAction, ViewActionTarget, ViewType } from '@kunlun/dependencies'; export class JumpActionWidget { protected goToObjectView() { executeViewAction( { viewType: ViewType.Form, moduleName: 'resource', model: 'resource.ResourceCountry', name: 'redirectUpdatePage', target: ViewActionTarget.Router } as RuntimeViewAction, undefined, undefined, { // 此处为id参数,目前只有表单和详情页需要 id: '12223', // 此处为上下文参数,context内对象的key是目标页面需要传递的默认值 context: JSON.stringify({code: 'xxx'}), // 此处为跳转后左侧菜单展开选中的配置 menu: JSON.stringify({"selectedKeys":["国家"],"openKeys":["地址库","地区"]}) } ); } protected goToListView() { const searchConditions: QueryExpression[] = []; searchConditions.push({ leftValue: ['countryCode'], // 查询条件的字段 operator: DefaultComparisonOperator.EQUAL, right: 'CN' // 字段的值 }); executeViewAction( { viewType: ViewType.Table, moduleName: 'resource', model: 'resource.ResourceCity', name: 'resource#市', target: ViewActionTarget.OpenWindow } as RuntimeViewAction, undefined, undefined, { // searchConditions相当于domain,不会随页面搜索项重置动作一起被清空 searchConditions: encodeURIComponent(JSON.stringify(searchConditions)), // searchBody的字段会填充搜索区域的字段组件,会随页面搜索项重置动作一起被清空 searchBody: JSON.stringify({code: 'CN'}), menu: JSON.stringify({"selectedKeys":["国家"],"openKeys":["地址库","地区"]}) } ); } } 扩展知识点 为什么executeViewAction跳转到的新页面不是入参的moduleName属性对应的模块? 答:跳转后所在的模块优先级为: 第一个入参的resModuleName属性对应的模块 执行executeViewAction时所在的模块 第一个入参的moduleName属性对应的模块 如何快速获取选中菜单的值? 答:先通过页面菜单手动打开页面,然后在浏览器自带调试工具的控制台执行decodeURIComponent(location.href),其中的menu参数就是我们需要的值

    2024年5月13日
    3.6K00
  • 从一个方法开始:浅析页面渲染流程

    渲染前的准备 渲染前的准备,在 Vue 渲染框架下,会先安装所有所支持的默认组件,比如 Mask,Header 等,这些组件支持 XML 默认模版的 Vue 框架下的渲染,详情可见 main.ts 中,maskInstall 与 install,这两个函数将平台内部支持的组件进行了注册,随后将整个 Vue 挂载为运行时 App,随后进行初始化。 渲染的起步 OioProvider 方法是整个应用的入口,我们忽略掉一些配置方法,将注意力集中到 initializeServices 从名称中我们可以看出来内部保存的都是初始化服务,其中提供了渲染服务等,我们当前使用的是 Vue 框架,所以当前其渲染的 Root 节点为 Vue, 以此,我们视野可以暂时转移到 admin-base中的 Root.vue以及 RootWidget上, 其实现了整个 Vue 框架下的 Root 节点如何渲染,其中定义了多个 widget,比如登陆页,首页,忘记密码已经重置密码等页面, 在本文中我们着重关注渲染首页的能力,RootWidget将 DefaultMetadataMainViewWidget作为渲染 Props中的 page即首页提供给下层组件使用, 渐入佳境 DefaultMetadataMainViewWidget从名称中可以看到,其为元数据主视图,主要做两件事,1:提供 Mask 的渲染2:提供元数据上下文初始化 该组件主要通过观察路由变化触发上面两个动作,当路由发生变化,该组件 reloadPage将被调用,reloadPage方法通过组装路由参数构成一个唯一 key 向后段查询当前路由所对应的渲染信息, 随后将获取到的信息进行处理,初始化,即 元数据上下文初始化,在初始化后,会将获取到的数据注入到 MetadataViewWidget中, Mask 的渲染 关于 Mask 的渲染,在获取到数据后,将生成 maskTemplate,并将其赋值, DefaultMetadataMainView.vue文件将渲染该模板,并渲染到页面中 数据的变更 当上面两件任务完成后,将开始主视图的渲染,上文提到,DefaultMetadataMainViewWidget只负责 mask 的渲染和上下文的初始化,所以 DefaultMetadataMainViewWidget通过触发事件的方式来实现主视图的渲染, DefaultMetadataMainViewWidget将必要信息作为事件参数触发,MultiTabsContainerWidget接收到 reloadMainViewCallChaining事件后,开启主视图渲染, MultiTabsContainerWidget会刷新运行时上下文,即 refreshRuntimeContext,该方法将尝试查询并通过 createOrUpdateEnterTab方法创建 Tab 页,createOrUpdateEnterTab最终生成一个 MultiTabItem格式的对象,该对象描述了 Tab 的相关信息,随后调用 createTabContainerWidget创建 tab 的容器即新建了一个 MultiTabContainerWidget组件即单个 tab 的容器,随后调用 setActiveTabItem, 并获取其绑定的 Vue 组件,并将其组件放置在 KeepAlive内部,触发更新, 主视图的渲染 MultiTabContainerWidget继承自MetadataViewWidget,当 MetadataViewWidget数据发生变更, 其绑定的 Vue 组件将解析 viewTemplate, 获取到与该模板 dslNodeType想匹配的 Vue 组件,当前例子中为 View.vue,随后 View.vue开始渲染,View.vue文件只是一个纯粹的容器,当 View.vue被挂载时,其内部的 template属性包含了整个页面的描述信息,View.vue需要做的就是将这个 template翻译并渲染成 DOM 展现在浏览器上, 渲染整个页面 当 View.vue被挂载时,其内部的 template属性包含了整个页面的描述信息,View.vue主要做了两个事情,一:将 template 中的 widget转换为组件,二:根据当前的 template信息生成 slot信息, const currentSlots = computed<Slots | undefined>( () => DslRender.fetchVNodeSlots(props.dslDefinition) || (Object.keys(context.slots).length ? context.slots : undefined) ); const renderWidget = createCustomWidget(InternalWidget.View, { …context.attrs, type: props.type || viewType.value, template: props.dslDefinition, metadataHandle: props.metadataHandle || metadataHandle.value, rootHandle: props.rootHandle || rootHandle.value, parentHandle: props.parentHandle || parentHandle.value, slotName: props.slotName, inline: inlineProp } as ViewWidgetProps); 生成这两部分信息后,View.vue会将这两部分挂载到页面上,这两部分从代码中可以看出,主要靠 fetchVNodeSlots,createCustomWidget两个函数, export function createCustomWidget( widget: string, props: CustomWidgetProps ): RenderWidget | undefined public static fetchVNodeSlots(dsl: DslDefinition | undefined, supportedSlotNames?: string[]):…

    2025年3月19日
    85500

Leave a Reply

Please Login to Comment