自定义组件之自动渲染(组件插槽的使用)(v4)

阅读之前

你应该:

自定义组件简介

前面我们简单介绍过一个简单的自定义组件该如何被定义,并应用于页面中。这篇文章将对自定义组件进行详细介绍。

自定义一个带有具名插槽的容器组件(一般用于Object数据类型的视图中)

使用BasePackWidget组件进行注册,最终体现在DSL模板中为<pack widget="SlotDemo">

SlotDemoWidget.ts
import { BasePackWidget, SPI } from '@kunlun/dependencies';
import SlotDemo from './SlotDemo.vue';

@SPI.ClassFactory(BasePackWidget.Token({ widget: 'SlotDemo' }))
export class SlotDemoWidget extends BasePackWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(SlotDemo);
    return this;
  }
}

定义一个Vue组件,包含三个插槽,分别是default不具名插槽title具名插槽footer具名插槽

SlotDemo.vue
<template>
  <div class="slot-demo-wrapper" v-show="!invisible">
    <div class="title">
      <slot name="title" />
    </div>
    <div class="content">
      <slot />
    </div>
    <div class="footer">
      <slot name="footer" />
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'SlotDemo',
  props: {
    invisible: {
      type: Boolean,
      default: undefined
    }
  }
});
</script>

在一个表单(FORM)的DSL模板中,我们可以这样使用这三个插槽:

<view type="FORM">
    <pack widget="SlotDemo">
        <template slot="default">
            <field data="id" />
        </template>
        <template slot="title">
            <field data="name" />
        </template>
        <template slot="footer">
            <field data="isEnabled" />
        </template>
    </pack>
</view>

这样定义的一个组件插槽和DSL模板就进行了渲染上的结合。

针对不具名插槽的特性,我们可以缺省slot="default"标签,缺少template标签包裹的所有元素都将被收集到default不具名插槽中进行渲染,则上述DSL模板可以改为:

<view type="FORM">
    <pack widget="SlotDemo">
        <field data="id" />
        <template slot="title">
            <field data="name" />
        </template>
        <template slot="footer">
            <field data="isEnabled" />
        </template>
    </pack>
</view>

自定义一个数组渲染组件(一般用于List数据类型的视图中)

由于表格无法体现DSL模板渲染的相关能力,因此我们以画廊视图(GALLERY)进行演示。

先定义一个数组每一项的数据结构:

typing.ts
export interface ListItem {
  key: string;
  data: Record<string, unknown>;
  index: number;
}
ListRenderDemoWidget.ts
import { BaseElementListViewWidget, BaseElementWidget, SPI } from '@kunlun/dependencies';
import ListRenderDemo from './ListRenderDemo.vue';

@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ListRenderDemo' }))
export class ListRenderDemoWidget extends BaseElementListViewWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(ListRenderDemo);
    return this;
  }
}
ListItemDemoWidget.ts
import { ActiveRecord, ActiveRecordsOperator, BaseElementWidget, SPI, Widget } from '@kunlun/dependencies';
import ListItemDemo from './ListItemDemo.vue';
import { ListItem } from './typing';

@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ListItemDemo' }))
export class ListItemDemoWidget extends BaseElementWidget {
  @Widget.Reactive()
  protected get formData(): ActiveRecord {
    return this.activeRecords?.[0] || {};
  }

  @Widget.Reactive()
  protected rowIndex: number | undefined;

  public initialize(props) {
    const slotContext = props.slotContext as ListItem;
    if (slotContext) {
      props.activeRecords = ActiveRecordsOperator.repairRecords(slotContext.data);
      this.rowIndex = slotContext.index;
    }
    super.initialize(props);
    this.setComponent(ListItemDemo);
    return this;
  }
}
ListRenderDemo.vue
<template>
  <div class="list-render-demo-wrapper">
    <div class="list-render-item-wrapper" v-for="item in list" :key="item.key">
      <slot v-bind="item" />
    </div>
  </div>
</template>

<script lang="ts">
import { ActiveRecord, uniqueKeyGenerator } from '@kunlun/dependencies';
import { computed, defineComponent, PropType } from 'vue';
import { ListItem } from './typing';

export default defineComponent({
  name: 'ListRenderDemo',
  props: {
    showDataSource: {
      type: Array as PropType<ActiveRecord[]>
    }
  },
  setup(props) {
    const list = computed<ListItem[]>(() => {
      return (
        props.showDataSource?.map((data, index) => {
          const item: ListItem = {
            key: data.__draftId || uniqueKeyGenerator(),
            data,
            index
          };
          return item;
        }) || []
      );
    });

    return {
      list
    };
  }
});
</script>
ListItemDemo.vue
<template>
  <div class="list-item-demo">
    <div class="title">
      <slot name="title" />
    </div>
    <div class="content">
      <slot />
    </div>
    <div class="footer">
      <slot name="footer" />
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'ListItemDemo'
});
</script>

在一个画廊(GALLERY)的DSL模板中,我们可以这样使用这三个插槽:

<view type="GALLERY">
    <template slot="gallery" widget="ListRenderDemo">
        <element widget="ListItemDemo">
            <template slot="default">
                <field data="id" />
            </template>
            <template slot="title">
                <field data="name" />
            </template>
            <template slot="footer">
                <field data="isEnabled" />
            </template>
        </element>
    </template>
</view>

PS:从默认提供的画廊视图(GALLERY)布局来看,我们可以通过slot="gallery"插槽完全替换下面的子标签,并指定widget="ListRenderDemo"来使用我们的自定义组件。

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

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

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

相关推荐

  • 前端自定义左树右表中的树

    在 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. 自定义…

    2024年10月21日
    2.2K00
  • 左树右表页面,点击表格的新建按钮,获取选中的树节点

    左树右表页面,点击表格的新建按钮,获取选中的树节点 通过自定义action的方式来实现 新建一个action文件TreeActionWidget.ts import { ActionType, ActionWidget, SPI, ViewActionTarget, RouterViewActionWidget } from '@kunlun/dependencies'; import { OioNotification } from '@kunlun/vue-ui-antd'; @SPI.ClassFactory( ActionWidget.Token({ actionType: [ActionType.View], target: [ViewActionTarget.Router], name: 'uiView0000000000079503' // action对应的name }) ) export class TreeActionWidget extends RouterViewActionWidget { protected async clickAction() { const context = this.rootRuntimeContext.view.context || {}; const activeTreeContext = context.activeTreeContext || null; if (!activeTreeContext) { // 没有选中左侧树 OioNotification.error('', '请选择左侧节点!'); } else { // 选中的时候 (this.action as any).context = activeTreeContext; // 执行原先的action逻辑 super.clickAction(); } } }

    2023年11月1日
    70300
  • 前端自定义字段与视图最佳方案

    自定义视图获取数据 在某些情况下,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…

    2024年10月17日
    1.5K00
  • oio-drawer抽屉

    屏幕边缘滑出的浮层面板。 何时使用 抽屉从父窗体边缘滑入,覆盖住部分父窗体内容。用户在抽屉内操作时不必离开当前任务,操作完成后,可以平滑地回到原任务。 当需要一个附加的面板来控制父窗体内容,这个面板在需要时呼出。比如,控制界面展示样式,往界面中添加内容。 当需要在当前任务流中插入临时任务,创建或预览附加内容。比如展示协议条款,创建子对象。 API 参数 说明 类型 默认值 版本 class 对话框外层容器的类名 string – closable 是否显示左上角的关闭按钮 boolean true closeIcon 自定义关闭图标 VNode | slot destroyOnClose 关闭时销毁 Drawer 里的子元素 boolean false footer 抽屉的页脚 VNode | slot – getTriggerContainer 指定 Drawer 挂载的 HTML 节点 HTMLElement | () => HTMLElement | Selectors ‘body’ height 高度, 在 placement 为 top 或 bottom 时使用 string | number keyboard 是否支持键盘 esc 关闭 boolean true mask 是否展示遮罩 Boolean true maskClosable 点击蒙层是否允许关闭 boolean true placement 抽屉的方向 ‘top’ | ‘right’ | ‘bottom’ | ‘left’ ‘right’ style 可用于设置 Drawer 最外层容器的样式,和 drawerStyle 的区别是作用节点包括 mask CSSProperties – title 标题 string | slot – visible(v-model:visible) Drawer 是否可见 boolean – width 宽度 string | number 378 zIndex 设置 Drawer 的 z-index Number 1000 cancelCallback 点击遮罩层或右上角叉或取消按钮的回调, return true则关闭弹窗 function(e) enterCallback 点击确定回调 function(e)

    2023年12月18日
    84100
  • 「前端」关闭源码依赖

    问题背景 在 5.x 版本的 oinone 前端架构中,我们开放了部分源码,但是导致以下性能问题: 1.构建耗时增加​​:Webpack 需要编译源码文件 2.​​加载性能下降​​:页面需加载全部编译产物 3.​​冷启动缓慢​​:开发服务器启动时间延长 以下方案通过关闭源码依赖。 操作步骤 1. 添加路径修正脚本 1: 在当前项目工程根目录中的scripts目录添加patch-package-entry.js脚本,内容如下: const fs = require('fs'); const path = require('path'); const targetPackages = [ '@kunlun+vue-admin-base', '@kunlun+vue-admin-layout', '@kunlun+vue-router', '@kunlun+vue-ui', '@kunlun+vue-ui-antd', '@kunlun+vue-ui-common', '@kunlun+vue-ui-el', '@kunlun+vue-widget' ]; // 递归查找目标包的 package.json function findPackageJson(rootDir, pkgName) { const entries = fs.readdirSync(rootDir); for (const entry of entries) { const entryPath = path.join(rootDir, entry); const stat = fs.statSync(entryPath); if (stat.isDirectory()) { if (entry.startsWith(pkgName)) { const [pkGroupName, name] = pkgName.split('+'); const pkgDir = path.join(entryPath, 'node_modules', pkGroupName, name); const pkgJsonPath = path.join(pkgDir, 'package.json'); if (fs.existsSync(pkgJsonPath)) { return pkgJsonPath; } } // 递归查找子目录 const found = findPackageJson(entryPath, pkgName); if (found) return found; } } return null; } // 从 node_modules/.pnpm 开始查找 const pnpmDir = path.join(__dirname, '../', 'node_modules', '.pnpm'); for (const pkgName of targetPackages) { const packageJsonPath = findPackageJson(pnpmDir, pkgName); if (packageJsonPath) { try { const packageJSON = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); if (packageJSON.main === 'index.ts') { const libName = packageJSON.name.replace('@', '').replace('/', '-'); packageJSON.main = `dist/${libName}.esm.js`; packageJSON.module = `dist/${libName}.esm.js`; const typings = 'dist/types/index.d.ts'; packageJSON.typings = typings; const [pwd] = packageJsonPath.split('package.json'); const typingsUrl = path.resolve(pwd, typings); const dir = fs.existsSync(typingsUrl); if (!dir)…

    2025年4月17日
    43200

Leave a Reply

登录后才能评论