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


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

    ① 布局容器组件

            

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

            定义文件

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

    1. // index.js
    2. import Container from './container.vue';
    3. class ContainerControl extends BaseControl {
    4. type = 'container';
    5. label = '布局容器';
    6. icon = 'tc-icon-layout';
    7. ...
    8. layout = {
    9. w: 30,
    10. h: 15,
    11. minH: 8,
    12. };
    13. // 组件实现的主文件
    14. DashboardComponent = Container;
    15. }
    16. export default new ContainerControl();

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

    具体来看看container.vue文件。

            组件主体

    1. // container.vue
    2. <template>
    3. <drag-container
    4. v-bind="fieldProps"
    5. @inChildComponent="$emit('inChildComponent', $event)"
    6. @add="handleAdd"
    7. @delete="handleDelete"
    8. @drop="syncDataToStore('add', $event)"
    9. >
    10. <drag-container-layout
    11. v-bind="fieldProps"
    12. :layout.sync="layout"
    13. :fields="fields"
    14. @resized="syncDataToStore('size', $event)"
    15. @moved="syncDataToStore('location', $event)"
    16. @edit="syncDataToStore('edit', $event)"
    17. @delete="syncDataToStore('delete', $event)"
    18. @select="handleSelect"
    19. />
    20. drag-container>
    21. template>

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

    1. // drag-container
    2. <template>
    3. <div
    4. @dragenter="dragenter"
    5. @dragover="dragover"
    6. @dragleave="dragleave"
    7. @drop="drop"
    8. >
    9. <slot />
    10. div>
    11. template>

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

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

    1. // drag-container-layout.vue
    2. <template>
    3. <grid-layout
    4. :layout.sync="layout"
    5. :col-num="60"
    6. :row-height="15"
    7. :isDraggable="!isRuntime"
    8. :isResizable="!isRuntime"
    9. :useCssTransforms="!isRuntime"
    10. >
    11. <template v-for="layoutItem in layout">
    12. <component
    13. v-if="isRuntime"
    14. :is="Item"
    15. :key="layoutItem.i"
    16. v-bind="getComponentProps(layoutItem)"
    17. />
    18. <grid-item
    19. v-else
    20. :key="layoutItem.i"
    21. v-bind="getLayoutProps(layoutItem)"
    22. @moved="$emit('moved', layoutItem)"
    23. @resized="$emit('moved', layoutItem)"
    24. @mousedown.native.stop="handlePointerDown"
    25. @mouseup.native.stop="handlePointerUp($event, layoutItem.i)"
    26. >
    27. <component
    28. :is="getComponent(layoutItem)"
    29. v-bind="getComponentProps(layoutItem)"
    30. @deleteComponent="handleDelete({ i: $event })"
    31. />
    32. grid-item>
    33. template>
    34. grid-layout>
    35. template>

            添加组件

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

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

    1. /** @name 进入-有效目标 **/
    2. dragenter() {
    3. if (this.limit) return;
    4. this.$emit('inChildComponent', true);
    5. }

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

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

    1. ** @name 移动-有效目标 **/
    2. dragover(e) {
    3. if (this.limit) return;
    4. e.preventDefault();
    5. e.dataTransfer.dropEffect = 'copy';
    6. this._dragover(e);
    7. }
    8. @throttle(100, { trailing: false })
    9. _dragover(e) {
    10. if (
    11. this.dragContext.clientX === e.clientX &&
    12. this.dragContext.clientY === e.clientY
    13. )
    14. return;
    15. // 时刻记录鼠标的位置
    16. this.dragContext.clientX = e.clientX;
    17. this.dragContext.clientY = e.clientY;
    18. this.updateInside(e);
    19. this.updateDrag(e);
    20. }
    21. /** @name 拖拽上下文,用于记录鼠标位置 */
    22. dragContext = {
    23. clientX: 0,
    24. clientY: 0,
    25. };

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

    1. /** @name 判断拖动元素是否在拖动区域内,是则添加一项(占位符),否则删除一项 **/
    2. updateInside(ev) {
    3. // 获取布局组件内部区域位置大小
    4. const rect = this.$el.getBoundingClientRect();
    5. // 容错率
    6. const errorRate = 10;
    7. // 判断拖动元素是否在拖动区域内
    8. const inside =
    9. ev.clientX > rect.left + errorRate &&
    10. ev.clientX < rect.right - errorRate &&
    11. ev.clientY > rect.top + errorRate &&
    12. ev.clientY < rect.bottom - errorRate;
    13. if (this.dragLayout) {
    14. if (inside) {
    15. this.$emit('add', deepClone(this.dragLayout));
    16. } else {
    17. this.$emit('delete', deepClone(this.dragLayout));
    18. }
    19. }
    20. }

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

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

            ②更新布局组件内部

    1. /** @name 离开-有效目标 **/
    2. dragleave(e) {
    3. if (this.limit) return;
    4. this.$emit('inChildComponent', false);
    5. this.updateInside(e);
    6. }

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

    1. /** @name 放置-有效目标 **/
    2. async drop() {
    3. if (this.limit) return;
    4. const dragLayout = deepClone(this.dragLayout);
    5. try {
    6. let field = createDashboardField(this.dragType);
    7. // 标记组件为子组件
    8. field.parentId = this.field.pkId;
    9. // 布局
    10. field.widget.layout = pick(dragLayout, 'x', 'y', 'w', 'h');
    11. // 添加到layout
    12. this.$emit(
    13. 'add',
    14. {
    15. ...field.widget.layout,
    16. i: field.pkId,
    17. },
    18. dragLayout.i,
    19. );
    20. this.$emit('drop', field);
    21. } catch (e) {
    22. this.$emit('delete', dragLayout);
    23. throw e;
    24. }
    25. }
    1. <drag-container
    2. ...
    3. @drop="syncDataToStore('add', $event)"
    4. >
    5. drag-container>

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

              删除组件

    1. // drag-container.vue
    2. /** @name 删除 **/
    3. handleDelete(layout) {
    4. this.$emit('delete', layout);
    5. }
    1. // container.vue
    2. <drag-container-layout
    3. ...
    4. @delete="syncDataToStore('delete', $event)"
    5. />

           放大缩小组件/ 改变位置     

            vue-grid-layout负责抛出

    1. <template v-for="layoutItem in layout">
    2. <grid-item
    3. ...
    4. @moved="$emit('moved', layoutItem)"
    5. @resized="$emit('sized', layoutItem)"
    6. >
    7. ...
    8. grid-item>

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

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

    1. // container.vue
    2. <drag-container-layout
    3. v-bind="fieldProps"
    4. :layout.sync="layout"
    5. :fields="fields"
    6. @resized="syncDataToStore('size', $event)"
    7. @moved="syncDataToStore('location', $event)"
    8. @delete="syncDataToStore('delete', $event)"
    9. />

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

            数据层处理

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

            

    1. /**
    2. * @name 同步到store
    3. * @param { String } type: 添加-add、删除-delete、大小变化-size、位置变化-moved
    4. * @param { Object } value: field、layout
    5. **/
    6. async syncDataToStore(type, value) {
    7. this.updateFields(fields => {
    8. const currentField = fields.find(field => field.pkId === this.field.pkId);
    9. const currentWidget = currentField.widget;
    10. if (type === 'add') {
    11. // 布局组件里面存储普通组件的字段
    12. currentWidget.fields.push(value);
    13. } else if (type === 'moved' || type === 'size') {
    14. // 移动会改变其他元素的位置, 所以整体要重复赋值x,y
    15. const layoutMap = generateMap(this.layout, 'i', layout => layout);
    16. currentWidget.fields.forEach(field => {
    17. field.widget.layout = pick(layoutMap[field.pkId], 'x', 'y', 'w', 'h');
    18. });
    19. } else if (type === 'delete') {
    20. const index = currentWidget.fields.findIndex(
    21. item => item.pkId === value.i,
    22. );
    23. currentWidget.fields.splice(index, 1);
    24. }
    25. return fields;
    26. });
    27. if (type === 'delete') {
    28. await this.$nextTick();
    29. // 记得更新视图,add就不用了,因为在dragover的时候已经更新了this.layout了
    30. this.syncLayout();
    31. }
    32. }

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

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

    1. <grid-item
    2. @mousedown.native.stop="handlePointerDown">grid-item>
    3. handlePointerDown(ev) {
    4. // 防止和父级选中冲突
    5. setTimeout(() => {
    6. this._pointerContext = {
    7. x: ev.clientX,
    8. y: ev.clientY,
    9. };
    10. });
    11. }

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

     

    ② 分页卡

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

  • 相关阅读:
    重制版day 10 字符串相关方法
    面向对象的三大特性: 继承,封装,多态
    Android Jetpack简介
    【毕业设计】水果图像识别系统 - 深度学习 OpenCV python
    Java数据结构—栈
    cs231n_2022的assignment-1实现(KNN部分)
    【学习笔记】CF1784F Minimums or Medians
    电大搜题——赋能学习,助力广东开放大学学子
    类android设备reset过程
    1.初识爬虫
  • 原文地址:https://blog.csdn.net/weixin_42274805/article/details/133500695