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

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

实现路径

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低代码应用平台体验

(0)
银时的头像银时数式员工
上一篇 2025年6月26日 pm3:10
下一篇 2025年7月8日 pm3:43

相关推荐

  • 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日
    67700
  • 前端自定义组件之多页面步骤条

    本文将讲解如何通过自定义,实现多页面的步骤条组件。其中每个步骤的元素里都对应界面设计器的一个页面。以下是代码实现和原理分析。 代码实现 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日
    51500
  • oio-checkbox 对选框

    API 属性 Checkbox 参数 说明 类型 默认值 版本 autofocus 自动获取焦点 boolean false checked(v-model:checked) 指定当前是否选中 boolean false disabled 失效状态 boolean false indeterminate 设置 indeterminate 状态,只负责样式控制 boolean false value 与 CheckboxGroup 组合使用时的值 boolean | string | number – 事件 事件名称 说明 回调参数 版本 change 变化时回调函数 Function(e:Event) –

    2023年12月18日
    72700
  • 前端页面嵌套

    我们可能会遇到这些需求,如:页面中的一对多字段不是下拉框,而是另一个模型的表单组;页面中的步骤条表单,每一步的表单都需要界面设计器设计,同时这些表单可能属于不同模型。这时候我们就可以采取页面嵌套的方式,在当前页面中,动态创建一个界面设计器设计的子页面。以一对多字段,动态创建表单子页面举例,以下是代码实现和原理分析。 代码实现 AddSubformWidget 动态添加表单 ts 组件 import { ModelFieldType, ViewType, SPI, BaseFieldWidget, Widget, FormO2MFieldWidget, ActiveRecord, CallChaining, createRuntimeContextByView, queryViewDslByModelAndName, uniqueKeyGenerator } from '@oinone/kunlun-dependencies'; import { MyMetadataViewWidget } from './MyMetadataViewWidget'; import { watch } from 'vue'; import AddSubform from './AddSubform.vue'; @SPI.ClassFactory( BaseFieldWidget.Token({ viewType: ViewType.Form, ttype: ModelFieldType.OneToMany, widget: 'AddSubform' }) ) export class AddSubformWidget extends FormO2MFieldWidget { public initialize(props) { super.initialize(props); this.setComponent(AddSubform); return this; } @Widget.Reactive() public myMetadataViewWidget: MyMetadataViewWidget[] = []; @Widget.Reactive() public myMetadataViewWidgetKeys: string[] = []; @Widget.Reactive() public myMetadataViewWidgetLength = 0; // region 子视图配置 public get subviewModel() { return this.getDsl().subviewModel || 'clm.contractcenter.ContractSignatory'; } public get subviewName() { return this.getDsl().subviewName || '签署方_FORM_uiViewa9c114903e104800b15e8f3749656b64'; } // region 添加子视图块 // 按钮添加点击事件 @Widget.Method() public async onAddSubviewBlock() { const resView = await queryViewDslByModelAndName(this.subviewModel, this.subviewName); this.createDynamicSubviewWidget(resView); } // 创建子视图块 public async createDynamicSubviewWidget(view, activeRecord: ActiveRecord = {}) { if (view) { // 根据视图构建上下文 const runtimeContext = createRuntimeContextByView( { type: ViewType.Form, model: view.model, modelName: view.modelDefinition.name, module: view.modelDefinition.module, moduleName: view.modelDefinition.moduleName, name: view.name, dsl: view.template }, true, uniqueKeyGenerator(), this.currentHandle ); // 取得上下文唯一标识 const runtimeContextHandle = runtimeContext.handle; const slotKey = `Form_${uniqueKeyGenerator()}`; // 创建子视图组件 const widget = this.createWidget(new MyMetadataViewWidget(runtimeContextHandle), slotKey, // 插槽名 { metadataHandle: runtimeContextHandle,…

    2025年7月21日
    59500
  • oio-select 选择器

    API Select props 参数 说明 类型 默认值 版本 allowClear 支持清除 boolean false autofocus 默认获取焦点 boolean false clearIcon 自定义的多选框清空图标 VNode | slot – disabled 是否禁用 boolean false dropdownClassName 下拉菜单的 className 属性 string – dropdownRender 自定义下拉框内容 ({menuNode: VNode, props}) => VNode | v-slot – filterOption 是否根据输入项进行筛选。当其为一个函数时,会接收 inputValue option 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false。 boolean | function(inputValue, option) true getTriggerContainer 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。 function(triggerNode) () => document.body menuItemSelectedIcon 自定义当前选中的条目图标 VNode | slot – options options 数据,如果设置则不需要手动构造 selectOption 节点 array<{value, label, [disabled, key, title]}> [] placeholder 选择框默认文字 string|slot – removeIcon 自定义的多选框清除图标 VNode | slot – suffixIcon 自定义的选择框后缀图标 VNode | slot – value(v-model:value) 指定当前选中的条目 string|string[]|number|number[] – 注意,如果发现下拉菜单跟随页面滚动,或者需要在其他弹层中触发 Select,请尝试使用 getPopupContainer={triggerNode => triggerNode.parentNode} 将下拉弹层渲染节点固定在触发器的父元素中。 事件 事件名称 说明 回调参数 blur 失去焦点的时回调 function change 选中 option,或 input 的 value 变化(combobox 模式下)时,调用此函数 function(value, option:Option/Array<Option>) deselect 取消选中时调用,参数为选中项的 value (或 key) 值,仅在 multiple 或 tags 模式下生效 function(value,option:Option) dropdownVisibleChange 展开下拉菜单的回调 function(open) focus 获得焦点时回调 function inputKeyDown 键盘按下时回调 function mouseenter 鼠标移入时回调 function mouseleave 鼠标移出时回调 function popupScroll 下拉列表滚动时的回调 function search 文本框值变化时回调 function(value: string) select 被选中时调用,参数为选中项的 value (或 key) 值 function(value, option:Option)

    2023年12月18日
    45600

Leave a Reply

登录后才能评论