前端
-
如何自定义表格单元格样式
介绍 OinOne的表格是基于Vxe-Table实现的,我们将Vxe-table内置的关于单元格样式的方法、属性开放到了表格组件TableWidget上 Vxe-Table相关文档 vxe-table的单元格样式 vxe-table的单元格动态样式 单元格样式 行的样式、单元格样式,表头的样式、表尾的样式、全部都可以完全自定义,通过设置 cellClassName、headerCellClassName、rowClassName …等参数 (注:当自定义样式之后可能会覆盖表格的样式,比如选中行..等,记得自行处理好相关样式) 单元格动态样式 行的动态样式、单元格动态样式,表头的动态样式、表尾的动态样式、可以通过设置 cellStyle、headerCellStyle、rowStyle …等参数 (注:当自定义样式之后可能会覆盖表格的样式,比如选中行..等,记得自行处理好相关样式) 示例代码 这里仅演示cellClassName和cellStyle,其他方法的出入参数请参考上面的Vxe-Table文档 import { BaseElementWidget, SPI, TableWidget, ViewType, Widget } from '@kunlun/dependencies'; @SPI.ClassFactory(BaseElementWidget.Token({ viewType: ViewType.Table, widget: 'CustomStyleTableWidget', })) export class CustomStyleTableWidget extends TableWidget { @Widget.Method() protected cellClassName({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) { if (column.field === 'field00019') { return `demo-cell-${column.field}`; } return ''; } @Widget.Method() protected cellStyle({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) { if (column.field === 'field00019') { return { backgroundColor: '#f60', color: '#ffffff' }; } return ''; } } 效果预览
-
前端自定义左树右表中的树
在 oinone 平台中,提供了默认的左树右表的视图,用户可以通过界面设计器配置,默认的树视图不一定满足所有需求,尤其当需要自定义功能或复杂的交互时,我们可以通过自定义视图来实现更灵活的展现。 本文将带你一步步了解如何自定义左树右表视图中的树组件。 自定义树视图 1. 使用界面设计器配置视图 首先,我们需要通过界面设计器生成基础的左树右表视图。界面设计器允许用户根据不同需求进行拖拽配置,快速创建可视化界面。 配置完视图之后,我们可以重写左侧的树组件。Oinone 的默认树组件是 TableSearchTreeWidget,通过自定义的方式,我们可以实现更高级的功能。 2. 重写 TableSearchTreeWidget import { BaseElementWidget, SPI, TableSearchTreeWidget, ViewType } from '@kunlun/dependencies'; import CustomTableSearchTree from './CustomTableSearchTree.vue'; @SPI.ClassFactory( BaseElementWidget.Token({ viewType: [ViewType.Table, ViewType.Form], widget: 'tree', model: 'resource.k2.Model0000000100' // 改成自己的模型 }) ) export class CustomTableSearchTreeWidget extends TableSearchTreeWidget { public initialize(props) { super.initialize(props); this.setComponent(CustomTableSearchTree); return this; } } 3. 定义 Vue 树组件 接下来,我们来实现 CustomTableSearchTree.vue 组件。这个组件将处理树的数据加载、节点选中等逻辑。你可以根据项目的需要修改其中的交互逻辑或 UI 设计。 <template> <a-tree :load-data="onLoadData" :tree-data="treeData" @select="onSelected" /> </template> <script lang="ts"> import { OioTreeNode, TreeUtils } from '@kunlun/dependencies'; import { computed, defineComponent } from 'vue'; export default defineComponent({ props: { rootNode: { type: Object }, loadData: { type: Function, required: true }, onSelected: { type: Function, required: true } }, setup(props) { // // 计算树的数据源,使用 TreeUtils 处理 const treeData = computed(() => { return TreeUtils.fillLoadMoreAction([…(props.rootNode?.children || [])]); }); // 异步加载子节点 const onLoadData = async (node) => { return await props.loadData(node.dataRef); }; // 处理节点选中事件 const onSelected = ( selectedKeys: string[], e: { nativeEvent: PointerEvent; node: { dataRef: OioTreeNode }; selected: boolean } ) => { props.onSelected?.(e.node.dataRef, e.selected); }; return { treeData, onLoadData, onSelected }; } }); </script> 4. 自定义…
-
前端自定义字段与视图最佳方案
自定义视图获取数据 在某些情况下,oinone 提供的默认视图无法满足需求,这时我们就需要自定义视图。通常,虽然视图的 UI 不足以满足要求,但数据结构是不变的。此时,重点是修改页面 UI,数据的请求与回填可以利用平台默认的能力。 如何实现? 界面设计器的使用 你可以通过界面设计器先配置页面,平台在运行时会根据设计器生成对应的 GraphQL 请求,并自动回填数据。 视图的数据结构 视图的数据类型分为可以分为 Object 跟 List Object 代表当前视图的数据结构是对象 List 代表当前视图的数据结构是数组。 如果我们将 Object 跟 List 分的更细一点就变成了这样: 1: Object: 对象,代表当前视图的数据结构是单个对象,例如:表单视图、详情视图1: List: 对象数组,代表当前视图的数据结构数组,例如:表格视图、卡片视图、画廊视图 视图类型 平台组件 数据属性 表格视图 TableWidget dataSource 画廊视图 GalleryWidget dataSource 表单视图 FormWidget formData 详情视图 DetailWidget formData 自定义视图时,需要先确认当前视图的类型,再通过界面设计器进行页面配置。前端部分只需继承相应的组件,平台底层会自动处理接口数据的获取与回填。 表单视图示例: import Form from './Form.vue'; // 自定义表单视图 @SPI.ClassFactory( BaseElementWidget.Token({ widget: 'custom-form' }) ) export class CustomForm extends FormWidget { public initialize(props: Props) { super.initialize(props); this.setComponent(Form); return this; } } Vue 组件: <template></template> <script lang="ts"> export default defineComponent({ props: { formData: { // 当前表单的数据 type: Object, default: () => ({}) } } }); </script> 自定义layout // 原始的layout模版 <view type="FORM"> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <element widget="form" slot="form"> <xslot name="fields" slotSupport="pack,field" /> </element> </view> //自定义的layout模版 <view type="FORM"> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <element widget="custom-form" slot="form"> <xslot name="fields" slotSupport="pack,field" /> </element> </view> 其实就是把 widget="form" 改成 widget="custom-form" 表格视图示例: import Table from './Table.vue'; // 自定义表格视图 @SPI.ClassFactory( BaseElementWidget.Token({ widget: 'custom-table' }) ) export class CustomTable extends TableWidget { public initialize(props: Props) { super.initialize(props); this.setComponent(Table); return this; } } Vue 组件: <template></template> <script…
-
列表视图、卡片视图切换
在日常项目开发中,我们可能会遇到当前视图是个表格,通过某个操作按钮将它变成卡片的形式. 这篇文章将带大家实现这种功能。通过上面两张图片可以看到出来不管是表格还是卡片,它都在当前的视图里面,所以我们需要一个视图容器来包裹表格跟卡片,并且表格可以使用平台默认的表格渲染,我们只需要自定义卡片即可。 我们用资源模块下面的国家菜单来实现这么一个功能这是对应的URL: http://localhost:8080/page;module=resource;viewType=TABLE;model=resource.ResourceCountry;action=resource%23%E5%9B%BD%E5%AE%B6;scene=resource%23%E5%9B%BD%E5%AE%B6;target=OPEN_WINDOW;menu=%7B%22selectedKeys%22:%5B%22%E5%9B%BD%E5%AE%B6%22%5D,%22openKeys%22:%5B%22%E5%9C%B0%E5%9D%80%E5%BA%93%22,%22%E5%9C%B0%E5%8C%BA%22%5D%7D 源码下载 views 创建外层的视图容器 刚刚我们讲过,不管是表格还是卡片,它都在当前的视图里面,所以我们需要写一个视图容器来包裹它们,并且对应的容器里面允许拆入表格跟卡片,我们先创建TableWithCardViewWidget.ts // TableWithCardViewWidget.ts import { BaseElementWidget, SPI, Widget } from '@kunlun/dependencies'; import TableWithCardView from './TableWithCardView.vue'; enum ListViewType { TABLE = 'table', CARD = 'card' } @SPI.ClassFactory( BaseElementWidget.Token({ widget: 'TableWithCardViewWidget' }) ) export class TableWithCardViewWidget extends BaseElementWidget { @Widget.Reactive() private listViewType: ListViewType = ListViewType.TABLE; // 当前视图展示的类型,是展示卡片还是表格 public initialize(props) { if (!props.slotNames) { props.slotNames = ['tableWidget', 'cardWidget']; } super.initialize(props); this.setComponent(TableWithCardView); return this; } } 在TableWithCardViewWidget中的initialize函数中,我们定义了两个插槽: tableWidget、cardWidget,所以需要在对应的vue文件里面里接收这两个插槽 <template> <div class="list-view-wrapper"> <!– 表格插槽 –> <div style="height: 100%" v-if="listViewType === 'table'"> <slot name="tableWidget" /> </div> <!– 卡片插槽 –> <div v-if="listViewType === 'card'"> <slot name="cardWidget"></slot> </div> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({ props: ['listViewType', 'onChangeViewType'], inheritAttrs: false, setup(props, context) { const onChangeViewType = (listViewType) => { props.onChangeViewType(listViewType); }; } }); </script> 这样一来,我们就定义好了视图容器,接下来就是通过自定义layout的方式注册该容器 layout注册 import { registerLayout, ViewType } from '@kunlun/dependencies'; registerLayout( `<view type="TABLE"> <pack widget="group"> <view type="SEARCH"> <element widget="search" cols="4" slot="search"/> </view> </pack> <pack widget="group" slot="tableGroup" style="position: relative"> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <element widget="TableWithCardViewWidget"> <template slot="tableWidget"> <element widget="table" slot="table" datasource-provider="true"> <element widget="expandColumn" slot="expandRow" /> <xslot name="fields" />…
-
表格如何支持表尾合计计算
介绍 可以通过扩展TableWidget.ts实现 示例代码 import { BaseElementWidget, DslDefinitionType, SPI, TableWidget, ViewType, Widget } from '@kunlun/dependencies'; @SPI.ClassFactory( BaseElementWidget.Token({ type: ViewType.Table, widget: 'table', model: 'resource.k2.Model0000000109', viewName: '移动端品牌_TABLE_0000000000021513' }) ) export class FooterStatisticsTable extends TableWidget { public initialize(props) { if (props.template) { props.template?.widgets?.forEach((a) => { if (a.dslNodeType === DslDefinitionType.FIELD && this.statisticsFieldList.includes(a.name)) { a.statistics = true; } }); } super.initialize(props); return this; } // 需要表尾做合并的字段名称 public statisticsFieldList = ['fansNum']; @Widget.Reactive() protected get showFooter(): boolean | undefined { return true; } } 效果预览
-
如何自定义点击导出动作绑定指定模板
介绍 平台默认的导出会打开弹窗,然后在弹窗内的视图选择是用模板方式导出还是选字段导出,但是有时候有部分场景希望点击导出动作后直接进入导出流程,导出指定的某个模板,我们可以通过覆写打开弹窗的动作来实现该功能。 本文档参考了 表格页自定义按钮如何获取搜索区域的查询条件 代码示例 以下代码大部分场景只需要修改其中excelTplName更换模板即可,另外如何想增加复用性,还可以将该属性改为从元数据的配置中获取。 import { ActionType, BaseActionWidget, BaseElementListViewWidget, BooleanHelper, ClickResult, Condition, ExcelExportTask, FILE_MODULE_NAME, getSessionPath, GraphqlHelper, http, IQueryPageResult, ISort, queryDslWidget, ReturnPromise, RuntimeServerAction, ServerActionWidget, SPI, SubmitValue, SYSTEM_MODULE_NAME, translateValueByKey, UrlHelper, ViewActionTarget, Widget } from '@kunlun/dependencies'; import { OioNotification } from '@kunlun/vue-ui-antd'; @SPI.ClassFactory( BaseActionWidget.Token({ actionType: [ActionType.View], target: [ViewActionTarget.Dialog], model: 'ys0328.k2.Model0000000453', name: 'internalGotoListExportDialog' }) ) export class DemoExportActionWidget extends ServerActionWidget { /** * excel导出模板名称 * @protected */ protected excelTplName = '演示抽屉跳转链接导出'; /** * 导出任务的模型编码 * @protected */ protected exportTaskModel = 'excelExportTask'; /** * 导出任务的方法 * @protected */ protected exportTaskFun = 'createExportTask'; /** * * 是否是同步导出 */ @Widget.Reactive() protected get syncExport() { return BooleanHelper.isTrue(this.getDsl().sync) || !true; } protected async executeAction(action: RuntimeServerAction, parameters: SubmitValue): Promise<ClickResult> { const workbookId = await this.getWorkbookId(); if (!workbookId) { return false; } let task = { workbookDefinition: { id: workbookId } } as ExcelExportTask; // 从平台内置的方法获取搜索区域的条件 const { condition } = this.getSearchRsqlAndQueryParams(); // 排序规则 let sortList = [] as ISort[]; const baseViewWidget = Widget.select(this.rootHandle); const listViewWidget = queryDslWidget(baseViewWidget?.getChildrenInstance(), BaseElementListViewWidget); if (listViewWidget) { sortList = listViewWidget.sortList; } return this.export(task, condition, sortList); } protected getUploadBodyGql(id: string, condition: string | Condition, sortList:…
-
前端视图的元数据与数据的传递、交互
在阅读本篇文章之前,您需要学习以下知识点: 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 是…
-
前端 SPI 注册 + 渲染
在阅读本篇文章之前,您需要学习以下知识点: 1: TS 结合 Vue 实现动态注册和响应式管理 前端开发者在使用 oinone 平台的时候会发现,不管是自定义字段还是视图,对应的 typescript 都会用到@SPI.ClassFactory(参数),然后在对用的class中重写initialize方法`: @SPI.ClassFactory(参数) export class CustomClass extends xxx { public initialize(props) { super.initialize(props); this.setComponent(FormString); return this; } } 本文将带您熟悉 oinone 前端的 SPI 注册机制以及 TS + Vue 的渲染过程。 不管是自定义字段还是视图@SPI.ClassFactory(参数)都是固定写法,区别在于参数不同,这篇文章里面详细描述了参数的定义。 SPI 注册机制 有自定义过字段、视图经验的开发者可能会发现,字段(表单字段)SPI 注册用的是FormFieldWidget.Token生成对应的参数,视图 SPI 注册用的是BaseElementWidget.Token,那么为什么要这样定义呢? 大家可以想象成现在有一个大的房子,房子里面有很多房间,每个房间都有自己的名字,比如FormFieldWidget就是房间的名字,BaseElementWidget也是房间的名字,这样一来我们就可以根据不同的房间存放不同的东西。 下面给大家展示下伪代码实现: class SPI { static container = new Map<string, WeakMap<object, object>>() static ClassFactory(token) { return (target) => { if(!SPI.container.get(token.type)) { SPI.container.set(token.type, new WeakMap()) } const services = SPI.container.get(token.type) services?.set(token, target) } } } class FormFieldWidget { static Token(options) { return { …options, type: 'Field' } } static Selector(options) { const fieldWidgets = SPI.container.get('Field') if(fieldWidgets) { return fieldWidgets.get(options)! } return null } } @SPI.ClassFactory(FormFieldWidget.Token({ viewType: 'Form', ttype: 'String', widget: 'Input' })) class StringWidget { } // 字段元数据 const fieldMeta = { name: "name", field: "name", mode: 'demo.model', widget: 'Input', ttype: 'String', viewType: 'Form' } // 找到对应的widget const widget = FormFieldWidget.Selector({ viewType: fieldMeta.viewType, ttype: fieldMeta.ttype, widget: fieldMeta.widget, }) 在上述代码中,我们主要是做了这么写事情: 1.SPI class class SPI { static container = new Map<string, WeakMap<object, object>>() } SPI 类是一个静态类,用于管理服务的注册和获取。 container 是一个静态属性,类型是 Map,它的键是字符串,值是 WeakMap。这个结构允许我们为每个服务类型(例如,Field)管理多个服务实例。 2.ClassFactory 方法 static ClassFactory(token) { return (target) => { if…
-
如何自定义 GraphQL 请求
在开发过程中,有时需要自定义 GraphQL 请求来实现更灵活的数据查询和操作。本文将介绍两种主要的自定义 GraphQL 请求方式:手写 GraphQL 请求和调用平台 API。 方式一:手写 GraphQL 请求 手写 GraphQL 请求是一种直接编写查询或变更语句的方式,适用于更复杂或特定的业务需求。以下分别是 query 和 mutation 请求的示例。 1. 手写 Query 请求 以下是一个自定义 query 请求的示例,用于查询某个资源的语言信息列表。 const customQuery = async () => { const query = `{ resourceLangQuery { queryListByEntity(query: {active: ACTIVE, installState: true}) { id name active installState code isoCode } } }`; const result = await http.query('resource', query); this.list = result.data['resourceLangQuery']['queryListByEntity']; }; 说明: query 语句定义了一个请求,查询 resourceLangQuery 下的语言信息。 查询的条件是 active 和 installState,只返回符合条件的结果。 查询结果包括 id、name、active、installState 等字段。 2. 手写 Mutation 请求 以下是一个 mutation 请求的示例,用于创建新的资源分类信息。 const customMutation = async () => { const code = Date.now() const name = `测试${code}` const mutation = `mutation { resourceTaxKindMutation { create(data: {code: "${code}", name: "${name}"}) { id code name createDate writeDate createUid writeUid } } }`; const res = await http.mutate('resource', mutation); console.log(res); }; 说明: mutation 语句用于创建一个新的资源分类。 create 操作的参数是一个对象,包含 code 和 name 字段。 返回值包括 id、createDate 等字段。 方式二:调用平台的 API 平台 API 提供了简化的 GraphQL 调用方法,可以通过封装的函数来发送 query 和 mutation 请求。这种方式减少了手写 GraphQL 语句的复杂性,更加简洁和易于维护。 1. 调用平台的 Mutation API 使用平台的 customMutation 方法可以简化 Mutation 请求。 /** * 自定义请求方法 * @param modelModel 模型编码 * @param method 方法名或方法对象 * @param records 请求参数,可以是单体对象或者对象的列表 * @param requestFields…
-
TS 结合 Vue 实现动态注册和响应式管理
基础知识 1: 面向对象 面向对象编程是 JavaScript 中一种重要的编程范式,它帮助开发者通过类和对象组织代码,提高代码的复用性。 2: 装饰器 装饰器在 JavaScript 中是用于包装 class 或方法的高阶函数 为了统一术语,下面的内容会把装饰器讲成注解 在 oinone 平台中,无论是字段还是动作,都是通过 ts + vue 来实现的,ts 中是面向对象的写法,所有的属性、方法建议都在对应的 class 中,如果有通用的属性跟方法,可以放在一个公共的 class 中,然后通过继承来实现,这样便于维护。 <!– FormString.vue –> <template> <div> <p>用户名: {{userName}}</p> <button @click="updateUser({name: '王五'})">修改用户名</button> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({ props: { userName: { type: String, default: '' }, userInfo: { type: Object, default: () => ({}) }, updateUser: { type: Function, default: () => () => ({}) } } }); </script> import FormString from './FormString.vue' @SPI.ClassFactory( FormFieldWidget.Token({ viewType: [ViewType.Form, ViewType.Search], ttype: ModelFieldType.String }) ) export class FormCustomStringFieldWidget extends FormFieldWidget { public initialize(props) { super.initialize(props); // 调用父类方法,确保继承的属性和方法正常初始化 this.setComponent(FormString); // 注册 Vue 组件,这样该 Widget 就会渲染 FormString 组件 return this; } public otherInfo = { name:'张三' } @Widget.Reactive() public userInfo = { name:'李四' } @Widget.Reactive() public get userName() { return this.userInfo.name } @Widget.Method() public updateUser userName(user) { this.userInfo = user } public updateOtherUser userName(user) { this.otherUser = user } } 这段代码定义了一个 FormCustomStringFieldWidget 类,用于处理表单中 String 类型字段的展示和交互。该类继承自 FormFieldWidget,并使用了多种注解和特性来实现不同功能。下面是对代码的详细讲解。 SPI 讲解 @SPI.ClassFactory() 无论是自定义字段还是动作,或者是自定义 mask、layout,都会用到@SPI.ClassFactory来注册,@SPI.ClassFactory 是一个注解,它标记了该类是通过工厂模式注册的。 在前端中,所有的注解(装饰器)本质上还是高阶函数,下面是一段伪代码。 const SPI = { ClassFactory(token) { return (targetClass) => {…