前端自定义组件之左右滑动

本文将讲解如何通过自定义,实现容器内的左右两个元素,通过左右拖拽分隔线,灵活调整宽度。其中左右元素里的内容都是界面设计器拖出来的。
前端自定义组件之左右滑动

实现路径

1. 界面设计器拖出页面

我们界面设计器拖个布局容器,然后在左右容器里拖拽任意元素。完成后点击右上角九宫格,选中布局容器,填入组件 api 名称,作用是把布局容器切换成我们自定义的左右滑动组件,这里的 api 名称和自定义组件的 widget 对应。最后发布页面,并绑定菜单。
前端自定义组件之左右滑动

2. 组件实现

widget 组件重写了布局容器,核心函数 renderLeftrenderRight,通过 DslRender.render 方法渲染界面设计器拖拽的元素。

import {
  BasePackWidget,
  DefaultContainersWidget,
  DslDefinition,
  DslRender,
  SPI,
  Widget
} from '@oinone/kunlun-dependencies';
import LeftRightSlide from './LeftRightSlide.vue';

// 拿到界面设计器配置的子容器元素
function fetchContainerChildren(widgets?: DslDefinition[], level = 3): DslDefinition[] {
  if (!widgets) {
    return [];
  }
  const children: DslDefinition[] = [];
  for (const widget of widgets) {
    if (widget.widget === 'container') {
      children.push(widget);
    } else if (level >= 1) {
      fetchContainerChildren(widget.widgets, level - 1).forEach((child) => children.push(child));
    }
  }
  return children;
}

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

  // 获取容器的子元素
  public get containerChildren(): DslDefinition[] {
    return fetchContainerChildren(this.template?.widgets);
  }

  // 初始宽度配置
  @Widget.Reactive()
  public get initialLeftWidth() {
    return this.getDsl().initialLeftWidth || 400;
  }
  // 最小左宽度配置
  @Widget.Reactive()
  public get minLeftWidth() {
    return this.getDsl().minLeftWidth || 200;
  }
  // 最小右宽度配置
  @Widget.Reactive()
  public get minRightWidth() {
    return this.getDsl().minRightWidth || 200;
  }

  // 根据容器子元素渲染左侧
  @Widget.Method()
  public renderLeft() {
    // 把容器的第一个元素作为左侧
    const containerLeft = this.containerChildren[0];
    if (containerLeft) {
      return DslRender.render(containerLeft);
    }
  }

  // 根据容器子元素渲染右侧
  @Widget.Method()
  public renderRight() {
    // 把容器的第二个元素作为右侧
    const containerRight = this.containerChildren[1];
    if (containerRight) {
      return DslRender.render(containerRight);
    }
  }
}

vue组件核心内容是用component :is属性,渲染出配置的容器组件

<template>
  <div class="LeftRightSlide">
    <div class="panel left-panel" :style="{ width: leftWidth + 'px' }">
      <component :is="leftComponent" />
    </div>
    <div class="splitter-bar" @mousedown="startDrag"></div>
    <div class="panel right-panel">
      <component :is="rightComponent" />
    </div>
  </div>
</template>

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

export default defineComponent({
  name: 'LeftRightSlide',
  components: {},
  props: {
    renderLeft: {
      type: Function
    },
    renderRight: {
      type: Function
    },
    initialLeftWidth: {
      type: Number,
      default: 400
    },
    minLeftWidth: {
      type: Number,
      default: 200
    },
    minRightWidth: {
      type: Number,
      default: 200
    }
  },
  setup(props) {
    const leftComponent = computed(() => props.renderLeft?.());
    const rightComponent = computed(() => props.renderRight?.());

    const leftWidth = ref(props.initialLeftWidth);

    const startDrag = (e: MouseEvent) => {
      const startX = e.clientX;
      const startWidth = leftWidth.value;

      const onMouseMove = (moveEvent: MouseEvent) => {
        const delta = moveEvent.clientX - startX;
        const newLeftWidth = Math.max(props.minLeftWidth, startWidth + delta);
        leftWidth.value = newLeftWidth;
      };

      const onMouseUp = () => {
        window.removeEventListener('mousemove', onMouseMove);
        window.removeEventListener('mouseup', onMouseUp);
      };

      window.addEventListener('mousemove', onMouseMove);
      window.addEventListener('mouseup', onMouseUp);
    };

    return {
      leftComponent,
      rightComponent,
      leftWidth,
      startDrag
    };
  }
});
</script>

<style lang="scss">
.LeftRightSlide {
  display: flex;
  height: 100%;
  width: 100%;
  overflow: hidden;
  position: relative;

  .panel {
    height: 100%;
    overflow: auto;
  }

  .left-panel {
  }

  .right-panel {
    flex: 1;
  }

  .splitter-bar {
    width: 6px;
    cursor: col-resize;
    background-color: #ccc;
    position: relative;
    z-index: 1;
    transition: background-color 0.2s;

    &:hover {
      background-color: #999;
    }
  }
}
</style>

3. 效果

前端自定义组件之左右滑动

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

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

Like (1)
银时's avatar银时数式员工
Previous 2025年6月26日 pm3:10
Next 2025年7月8日 pm3:43

相关推荐

  • oio-empty-data 空数据状态

    何时使用 当目前没有数据时,用于显式的用户提示。 初始化场景时的引导创建流程。 API 参数 说明 类型 默认值 版本 description 自定义描述内容 string | v-slot – image 设置显示图片,为 string 时表示自定义图片地址 string | v-slot false imageStyle 图片样式 CSSProperties –

    2023年12月18日
    1.7K00
  • oio-switch 开关

    API 参数 说明 类型 默认值 版本 autofocus 组件自动获取焦点 boolean false checked(v-model: checked ) 指定当前是否选中 checkedValue | unCheckedValue false checkedChildren 选中时的内容 slot checkedValue 选中时的值 boolean | string | number true disabled 是否禁用 boolean false loading 加载中的开关 boolean false unCheckedChildren 非选中时的内容 slot unCheckedValue 非选中时的值 boolean | string | number false 事件 事件名称 说明 回调参数 change 变化时回调函数 Function(checked: boolean | string | number, event: Event)

    2023年12月18日
    1.5K00
  • 前端表格复制

    我们可能会遇到表格复制的需求,也就是表格填写的时候,不是增加一行数据,而是增加一个表格。以下是代码实现和原理分析。 代码实现 在 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日
    89500
  • 前端自定义组件之多页面步骤条

    本文将讲解如何通过自定义,实现多页面的步骤条组件。其中每个步骤的元素里都对应界面设计器的一个页面。以下是代码实现和原理分析。 代码实现 NextStepWidget 多页面步骤条 ts 组件 import { CallChaining, createRuntimeContextByView, customMutation, customQuery, RuntimeView, SPI, ViewCache, Widget, DefaultTabsWidget, BasePackWidget } from '@oinone/kunlun-dependencies'; import { isEmpty } from 'lodash-es'; import { MyMetadataViewWidget } from './MyMetadataViewWidget'; import NextStep from './NextStep.vue'; import { IStepConfig, StepDirection } from './type'; @SPI.ClassFactory(BasePackWidget.Token({ widget: 'NextStep' })) export class NextStepWidget extends DefaultTabsWidget { public initialize(props) { this.titles = props.template?.widgets?.map((item) => item.title) || []; props.template && (props.template.widgets = []); super.initialize(props); this.setComponent(NextStep); return this; } @Widget.Reactive() public get invisible() { return false; } // 配置的每一步名称 @Widget.Reactive() public titles = []; // region 上一步下一步配置 // 步骤配置,切换的顺序就是数组的顺序,模型没有限制 @Widget.Reactive() public get stepJsonConfig() { let data = JSON.parse( this.getDsl().stepJsonConfig || '[{"model":"resource.ResourceCountry","viewName":"国家form"},{"model":"resource.ResourceProvince","viewName":"省form"},{"model":"resource.ResourceCity","viewName":"市form"}]' ); return data as IStepConfig[]; } // 切换上一步下一步 @Widget.Method() public async onStepChange(stepDirection: StepDirection) { // 没有激活的,说明是初始化,取第一步 if (!this.activeStepKey) { const step = this.stepJsonConfig[0]; if (step) { this.activeStepKey = `${step.model}_${step.viewName}`; await this.initStepView(step); } return; } // 获取当前步骤的索引 if (this.currentActiveKeyIndex > -1) { await this.onSave(); // 获取下一步索引 const nextStepIndex = stepDirection === StepDirection.NEXT ? this.currentActiveKeyIndex + 1 : this.currentActiveKeyIndex – 1; // 在索引范围内,则渲染视图 if (nextStepIndex >= 0 && nextStepIndex < this.stepJsonConfig.length) { const nextStep = this.stepJsonConfig[nextStepIndex];…

    2025年7月21日
    92800
  • oio-pagination 分页

    API 参数 说明 类型 默认值 版本 currentPage(v-model:currentPage) 当前页数 number – defaultPageSize 默认的每页条数 number 15 disabled 禁用分页 boolean – pageSize 每页条数 number – pageSizeOptions 指定每页可以显示多少条 string[] [’10’, ’15’, ’30’, ’50’, ‘100’, ‘200’] showQuickJumper 是否可以快速跳转至某页 boolean false showSizeChanger 是否展示 pageSize 切换器,当 total 大于 50 时默认为 true boolean – total 数据总数 number 0 事件 事件名称 说明 回调参数 change 页码或 pageSize 改变的回调,参数是改变后的页码及每页条数 Function(page, pageSize) noop

    2023年12月18日
    1.6K00

Leave a Reply

Please Login to Comment