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

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

实现路径

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

相关推荐

  • 前端自定义组件之多页面步骤条

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

    全局展示通知提醒信息。 何时使用 在系统四个角显示通知提醒信息。经常用于以下情况: 较为复杂的通知内容。 带有交互的通知,给出用户下一步的行动点。 系统主动推送。 API OioNotification.success(title,message, config) OioNotification.error(title,message, config) OioNotification.info(title,message, config) OioNotification.warning(title,message, config) config 参数如下: 参数 说明 类型 默认值 版本 duration 默认 3 秒后自动关闭 number 3 class 自定义 CSS class string –

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

    我们可能会遇到这些需求,如:页面中的一对多字段不是下拉框,而是另一个模型的表单组;页面中的步骤条表单,每一步的表单都需要界面设计器设计,同时这些表单可能属于不同模型。这时候我们就可以采取页面嵌套的方式,在当前页面中,动态创建一个界面设计器设计的子页面。以一对多字段,动态创建表单子页面举例,以下是代码实现和原理分析。 代码实现 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日
    46400
  • oio-dropdown 下拉菜单

    向下弹出的列表。 何时使用 当页面上的操作命令过多时,用此组件可以收纳操作元素。点击或移入触点,会出现一个下拉菜单。可在列表中进行选择,并执行相应的命令。 用于收罗一组命令操作。 Select 用于选择,而 Dropdown 是命令集合。 API 属性如下 参数 说明 类型 默认值 destroyOnHide 关闭后是否销毁 Dropdown boolean false disabled 菜单是否禁用 boolean – getTriggerContainer 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。 Function(triggerNode) () => document.body overlay(v-slot) 菜单 Menu – overlayClassName 下拉根元素的类名称 string – overlayStyle 下拉根元素的样式 object – placement 菜单弹出位置 bottomLeft | bottom | bottomRight | topLeft | top | topRight bottomLeft trigger 触发下拉的行为, 移动端不支持 hover Array<click|hover|contextmenu> ['hover'] visible(v-model:visible) 菜单是否显示 boolean – 事件 事件名称 说明 回调参数 onUpdateValue 菜单显示状态改变时调用,参数为 visible。点击菜单按钮导致的消失不会触发 function(visible)

    2023年12月18日
    65700
  • oio-input 输入框

    代码演示 <oio-input v-model:value="value"></oio-input> API Input 参数 说明 类型 默认值 版本 addonAfter 带标签的 input,设置后置标签 string|slot addonBefore 带标签的 input,设置前置标签 string|slot allowClear 可以点击清除图标删除内容 boolean defaultValue 输入框默认内容 string disabled 是否禁用状态,默认为 false boolean false maxlength 最大长度 number prefix 带有前缀图标的 input slot showCount 是否展示字数 boolean false suffix 带有后缀图标的 input slot type 声明 input 类型,同原生 input 标签的 type 属性,见:MDN(请直接使用 <a-textarea /> 代替 type="textarea")。 string text value(v-model:value) 输入框内容 string Input 事件 事件名称 说明 回调参数 update:value 输入框内容变化时的回调 function(e) pressEnter 按下回车的回调 function(e) Input.Search 代码演示 <oio-input-search v-model:value="value"></oio-input-search> Input.Search 事件 事件名称 说明 回调参数 search 点击搜索或按下回车键时的回调 function(value, event) 其余属性和 Input 一致。

    2023年12月18日
    1.7K00

Leave a Reply

登录后才能评论