讲讲项目里的仪表盘编辑器(四)分页卡和布局容器组件

讲讲两个经典布局组件的实现

① 布局容器组件

配置面板是给用户配置布局容器背景颜色等属性。这里我们不需要关注

定义文件

规定了组件类的类型、标签、图标、默认布局属性、主文件等等。

javascript 复制代码
// index.js
import Container from './container.vue';
class ContainerControl extends BaseControl {
     type = 'container';

     label = '布局容器';

     icon = 'tc-icon-layout';
    
     ...
        
     layout = {
        w: 30,
        h: 15,
        minH: 8,
     };
         
     // 组件实现的主文件
     DashboardComponent = Container;
}

export default new ContainerControl();

入口文件会通过一系列逻辑生成【类型枚举类】,我们最后通过control['container'].DashboardComponent找到主体文件生成组件。这些我们简单了解就好啦。

具体来看看container.vue文件。

组件主体

html 复制代码
// container.vue
<template>
  <drag-container
    v-bind="fieldProps"
    @inChildComponent="$emit('inChildComponent', $event)"
    @add="handleAdd"
    @delete="handleDelete"
    @drop="syncDataToStore('add', $event)"
  >
    <drag-container-layout
      v-bind="fieldProps"
      :layout.sync="layout"
      :fields="fields"
      @resized="syncDataToStore('size', $event)"
      @moved="syncDataToStore('location', $event)"
      @edit="syncDataToStore('edit', $event)"
      @delete="syncDataToStore('delete', $event)"
      @select="handleSelect"
    />
  </drag-container>
</template>

这里的drag-container其实长这样:

html 复制代码
// drag-container
<template>
  <div
    @dragenter="dragenter"
    @dragover="dragover"
    @dragleave="dragleave"
    @drop="drop"
  >
    <slot />
  </div>
</template>

是不是很熟悉?对,就是上一章讲的包裹着组件的drag事件层。用来触发inChildComponent事件的。

drag-container-layout其实就是一个 grid-layout。有运行时和设计时两种情况(设计时可以拖拽组件进去,运行时只是纯展示)

html 复制代码
// drag-container-layout.vue
<template>
  <grid-layout
    :layout.sync="layout"
    :col-num="60"
    :row-height="15"
    :isDraggable="!isRuntime"
    :isResizable="!isRuntime"
    :useCssTransforms="!isRuntime"
  >
    <template v-for="layoutItem in layout">
      <!-- 运行时 -->
      <component
        v-if="isRuntime"
        :is="Item"
        :key="layoutItem.i"
        v-bind="getComponentProps(layoutItem)"
      />
      <!-- 设计时 -->
      <grid-item
        v-else
        :key="layoutItem.i"
        v-bind="getLayoutProps(layoutItem)"
        @moved="$emit('moved', layoutItem)"
        @resized="$emit('moved', layoutItem)"
        @mousedown.native.stop="handlePointerDown"
        @mouseup.native.stop="handlePointerUp($event, layoutItem.i)"
      >
            <component
              :is="getComponent(layoutItem)"
              v-bind="getComponentProps(layoutItem)"
              @deleteComponent="handleDelete({ i: $event })"
            />
      </grid-item>
    </template>
  </grid-layout>
</template>

添加组件

上一节我们已经将过点击添加到布局组件内,所以这节主要展开讲讲拖拽。逻辑跟上一节会有一些不一样,上一节主要还是为了方便理解。

拖拽组件进入布局组件内部时,drag-container层首先响应。触发dragenter事件

javascript 复制代码
  /** @name 进入-有效目标 **/
  dragenter() {
    if (this.limit) return;
    this.$emit('inChildComponent', true);
  }

当拖拽进来的组件是布局组件时,this.limit为true。这里的业务逻辑是不允许多层嵌套所以在这里做了阻断。此时不会给外界传递inChildComponent事件,仪表盘的gird-layout也不需要改变this.isInChildCom。这里跟上一节讲的不一样,是因为vue-grid-layout这个组件本身不允许组件之间重叠(组件是有碰撞体积的)。所以即使它进入到布局组件内,布局组件内不接管,也会被插件阻拦。

同时触发dragover事件,为了定位拖拽的组件在布局组件内的位置

javascript 复制代码
** @name 移动-有效目标 **/
dragover(e) {
    if (this.limit) return;
    e.preventDefault();
    e.dataTransfer.dropEffect = 'copy';
    this._dragover(e);
}

@throttle(100, { trailing: false })
_dragover(e) {
    if (
      this.dragContext.clientX === e.clientX &&
      this.dragContext.clientY === e.clientY
    )
      return;
    // 时刻记录鼠标的位置
    this.dragContext.clientX = e.clientX;
    this.dragContext.clientY = e.clientY;
    this.updateInside(e);
    this.updateDrag(e);
}

/** @name 拖拽上下文,用于记录鼠标位置 */
dragContext = {
    clientX: 0,
    clientY: 0,
};

updateInside是为了在拖动的时候更新布局组件内的布局,让拖动元素在布局组件内部形成占位符。这一点在之前几章我都没讲过,是因为vue-grid-layout这个组件对拖拽效果已经做了很好的处理了,此时加上拖拽时占位,只不过是锦上添花的效果罢了。

javascript 复制代码
  /** @name 判断拖动元素是否在拖动区域内,是则添加一项(占位符),否则删除一项 **/
  updateInside(ev) {
    // 获取布局组件内部区域位置大小
    const rect = this.$el.getBoundingClientRect();
    // 容错率
    const errorRate = 10;
    // 判断拖动元素是否在拖动区域内
    const inside =
      ev.clientX > rect.left + errorRate &&
      ev.clientX < rect.right - errorRate &&
      ev.clientY > rect.top + errorRate &&
      ev.clientY < rect.bottom - errorRate;
    if (this.dragLayout) {
      if (inside) {
        this.$emit('add', deepClone(this.dragLayout));
      } else {
        this.$emit('delete', deepClone(this.dragLayout));
      }
    }
  }

add和delete最终指向是操作drag-container-layout.vue里的this.layout这个属性,也就是布局容器内的布局(add操作会查找this.layout是否重复存在这个拖拽元素)。可以理解为dragover操控更新了布局容器内的布局,而一旦dragleave,则会:

①取消接管仪表盘layout层的拖拽事件。恢复到仪表盘layout层进行接管

②更新布局组件内部

javascript 复制代码
  /** @name 离开-有效目标 **/
dragleave(e) {
   if (this.limit) return;
   this.$emit('inChildComponent', false);
   this.updateInside(e);
}

那么最最最关键的一环,无非是drop事件了。它的核心思路是把布局容器当前的layout里的draglayout拿出来,将它的位置属性记录在生成的拖拽组件属性中。并抛出到vuex仓库里进行存储。如果失败,也只需要删除视图层layout里的dragLayout组件罢了。

javascript 复制代码
 /** @name 放置-有效目标 **/
  async drop() {
    if (this.limit) return;
    const dragLayout = deepClone(this.dragLayout);
    try {
      let field = createDashboardField(this.dragType);
      // 标记组件为子组件
      field.parentId = this.field.pkId;
      // 布局
      field.widget.layout = pick(dragLayout, 'x', 'y', 'w', 'h');
      // 添加到layout
      this.$emit(
        'add',
        {
          ...field.widget.layout,
          i: field.pkId,
        },
        dragLayout.i,
      );
      this.$emit('drop', field);
    } catch (e) {
      this.$emit('delete', dragLayout);
      throw e;
    }
  }
html 复制代码
<drag-container
    ...
    @drop="syncDataToStore('add', $event)"
>
</drag-container>

这个syncDataToStore方法会吧数据同步到vuex仓库,包括了新增/删除/变化。我们最后再讲。到这一步,我们已经把视图层关于新增的步骤完成了。

删除组件

javascript 复制代码
// drag-container.vue
/** @name 删除 **/
handleDelete(layout) {
    this.$emit('delete', layout);
}
html 复制代码
// container.vue    
<drag-container-layout
      ...
      @delete="syncDataToStore('delete', $event)"
 />

放大缩小组件/ 改变位置

vue-grid-layout负责抛出

html 复制代码
<template v-for="layoutItem in layout">
    <grid-item
       ...
        @moved="$emit('moved', layoutItem)"
        @resized="$emit('sized', layoutItem)"
    >
        ...
    </grid-item>
</template >

这里很巧妙的运用了this.layout属性,vue-grid-layout的官方示例用法是这样的:

可以理解为这两个响应事件是返回了新的位置信息。而项目里的写法是利用了vue-grid-layout在moved或resized之后自身的this.layout也会随着改变,里面的layout-item也会跟随动态变化,所以直接把layout-item当做参数传出

html 复制代码
// container.vue

    <drag-container-layout
      v-bind="fieldProps"
      :layout.sync="layout"
      :fields="fields"
      @resized="syncDataToStore('size', $event)"
      @moved="syncDataToStore('location', $event)"
      @delete="syncDataToStore('delete', $event)"
    />

和添加组件一样,视图层逻辑到此结束,等待数据层处理

数据层处理

每个项目都有自己的处理方式,到这里视图层已经完成了自己的使命,把数据教辅给数据层进行存储变更。所以参考一下就行啦

javascript 复制代码
  /**
   * @name 同步到store
   * @param { String } type: 添加-add、删除-delete、大小变化-size、位置变化-moved
   * @param { Object } value: field、layout
   **/
  async syncDataToStore(type, value) {
    this.updateFields(fields => {
      const currentField = fields.find(field => field.pkId === this.field.pkId);
      const currentWidget = currentField.widget;
      if (type === 'add') {
        // 布局组件里面存储普通组件的字段
        currentWidget.fields.push(value);
      } else if (type === 'moved' || type === 'size') {
        // 移动会改变其他元素的位置, 所以整体要重复赋值x,y
        const layoutMap = generateMap(this.layout, 'i', layout => layout);
        currentWidget.fields.forEach(field => {
          field.widget.layout = pick(layoutMap[field.pkId], 'x', 'y', 'w', 'h');
        });
      } else if (type === 'delete') {
        const index = currentWidget.fields.findIndex(
          item => item.pkId === value.i,
        );
        currentWidget.fields.splice(index, 1);
      }
      return fields;
    });
    if (type === 'delete') {
      await this.$nextTick();
      // 记得更新视图,add就不用了,因为在dragover的时候已经更新了this.layout了
      this.syncLayout();
    }
  }

特别注意的是,移动位置或 更改大小需要更新容器内所有组件的位置,因为可能会发生挤压或换行。

区分父容器和布局容器里的点击事件

html 复制代码
<grid-item
@mousedown.native.stop="handlePointerDown"></grid-item>

handlePointerDown(ev) {
    // 防止和父级选中冲突
    setTimeout(() => {
      this._pointerContext = {
        x: ev.clientX,
        y: ev.clientY,
      };
    });
  }

settimeout(fn,0)会让方法在在下一轮"事件循环"开始时执行。从而避免与父容器冲突。

② 分页卡

跟布局容器一样,只是数据存储多了一层嵌套

相关推荐
无责任此方_修行中1 小时前
每周见闻分享:杂谈AI取代程序员
javascript·资讯
Σίσυφος19002 小时前
halcon 条形码、二维码识别、opencv识别
前端·数据库
学代码的小前端2 小时前
0基础学前端-----CSS DAY13
前端·css
dorabighead3 小时前
JavaScript 高级程序设计 读书笔记(第三章)
开发语言·javascript·ecmascript
css趣多多3 小时前
案例自定义tabBar
前端
mcusun20004 小时前
VScode 使用Deepseek又方便又好用的另一款插件
ide·vscode·编辑器·deepseek
姑苏洛言4 小时前
DeepSeek写微信转盘小程序需求文档,这不比产品经理强?
前端
林的快手4 小时前
CSS列表属性
前端·javascript·css·ajax·firefox·html5·safari
leoufung5 小时前
VIM FZF 安裝和使用
linux·编辑器·vim
匹马夕阳5 小时前
ECharts极简入门
前端·信息可视化·echarts