讲讲项目里的仪表盘编辑器(一)

需求

        要做一个仪表盘系统,要求有:

        ① 设计功能(包括布局、大小、排列)
        ② 预览功能
        ③ 运行功能

 布局选择

        做编辑器,肯定要先选择布局。

        前端有几种常用布局。

       静态布局

        也叫文档布局。默认的网页形式是文档流的形式,一个网页就像是一条条从左流向右的河流。在文档中有两种元素,内联元素(display:inline)和块级元素(display:block)。内联元素默认从左到右流,遇到阻碍或者宽度不够自动换行,继续按照从左到右的方式布局。而块级元素则会独占一行,按照从上往下的方式布局。内联元素的宽度和高度默认都被内容撑开。

        这肯定不适合用来做编辑器。

        浮动布局

        浮动元素会脱离文档流并浮动到左侧或右侧。这时候其他的周围内容就会在这个被设置浮动 (float) 的元素周围环绕。

               这种肯定也不行。因为元素只能向左向右浮动。并且周围元素会围绕浮动元素旋转,并不会脱离文档流,也就是无法自定义其位置。

        定位布局

        通过调整position这个CSS属性的值来实现(absolute/relative/fixed/static(绝对/相对/固定/静态(默认))四个值)。

        static为静态布局,遵循默认文档流。这没什么好说的。

        absolute绝对定位:元素会脱离文档流,通过TBLR( top,bottom,left,right) 定位,会选取最近的一个有定位设置的父级对象(非static)进行绝对定位,如果没有设置定位属性的父级对象,则将以body坐标原点进行定位。绝对定位的元素不会占有空间,也不会影响别的元素。

        relative相对定位:对象不可层叠、不脱离文档流,参考自身静态位置通过 TBLR定位。设置了TBLR之后,元素位置会发生偏移,但仍然占有原来的位置,且不会影响别的元素,而是覆盖在上方。相对定位与绝对定位的区别就是相对定位是只占有原来的空间,而绝对定位不占有空间。

        fixed固定定位:顾名思义,固定定位就是固定在一个位置,不会随着页面滚动而改变位置的定位方式,像常见的页面上的小广告,或者右下角的返回页面顶部等等。

        如果要做编辑器,fixed(只有单页了)和static肯定不行。relative不脱离文档流,也不行。那么只能以页面为定位的absolute绝对定位了。这么做有几个缺点:

        第一,每个元素都需要精准的TBLR值。这是很麻烦的。甚至需要去计算。

        第二:拖拽放大缩小元素的时候,会改变其自身的TBLR值。并且可能会盖到其他元素上面去。我们需要的效果是像果冻一样挤压其他元素。使其挪到后面或下面。

        瀑布布局

        视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。

         瀑布流的实现方法决定了它的元素排序,具体请看我这篇推文

瀑布流布局的实现_AI3D_WebEngineer的博客-CSDN博客瀑布流布局视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部​​ 瀑布流的实现方法决定了它的元素排序,具体请看我这篇推文。https://blog.csdn.net/weixin_42274805/article/details/132981042

         

        flex布局

         弹性盒模型。也是纯CSS布局。具体可以看廖雪峰的Flex 布局教程:语法篇 - 阮一峰的网络日志icon-default.png?t=N7T8https://www.ruanyifeng.com/blog/2015/07/flex-grammar.html

        但是做编辑器是不能选择flex布局的,为什么?请看下面Grid布局的介绍 

                

        Grid布局

        网络布局,目前唯一一种 CSS 二维布局。擅长将一个页面划分为几个主要区域,以及定义这些区域的大小、位置、层次等关系。

        与flex布局的区别:flex 布局是一维布局,Grid 布局是二维布局。flex 布局一次只能处理一个维度上的元素布局,一行或者一列。Grid 布局是将容器划分成了“行”和“列”,产生了一个个的网格,我们可以将网格元素放在与这些行和列相关的位置上,从而达到我们布局的目的。

  

        布局实现
<body>
    <div class="container">
        <div class="item">
            <img src="img/bg1.png" alt="" />
        </div>
        <div class="item">
            <img src="img/bg2.png" alt="" />
        </div>
        <div class="item">
            <img src="img/bg3.png" alt="" />
        </div>
        <div class="item">
            <img src="img/bg4.png" alt="" />
        </div>
        <div class="item">
            <img src="img/bg5.png" alt="" />
        </div>
        <div class="item">
            <img src="img/bg6.png" alt="" />
        </div>
    </div>
</body>

        此时我们只需要对container这个父容器添加grid属性就好:

<style>
    .container {
        /* 声明一个容器 */
        display: grid;
        /*  声明列的宽度  */
        grid-template-columns: repeat(3, 200px);
        /*  声明行间距和列间距  */
        grid-gap: 20px;
        /*  声明行的高度  */
        grid-template-rows: 100px 200px;
    }

    .item img {
        width: 100%;
        height: 100%;
    }
</style>

    grid-template-columns: repeat(3, 200px);

    这里的2是分为3列的意思,200px是列宽

 

       

        容器

         我们通过在元素上声明 display:grid 或 display:inline-grid 来创建一个网格容器。一旦我们这样做,这个元素的所有直系子元素将成为网格项目。比如上面 .container所在的元素为一个网格容器,其直系子元素将成为网格项目。 

        网格轨道

        grid-template-columns 和 grid-template-rows 属性来定义网格中的行和列。容器内部的水平区域称为行,垂直区域称为列。

        

        网格单元

        一个网格单元是在一个网格元素中最小的单位

        1 2 3 4 5 6 各是一个网络单元

        网格线

        划分网格的线,称为"网格线"。这对我们来说是不可见的。Grid 会为我们创建编号的网格线来让我们来定位每一个网格元素。m 列有 m + 1 根垂直的网格线,n 行有 n + 1 跟水平网格线。

        详细的语法请看:

最强大的 CSS 布局 —— Grid 布局 - 掘金Grid 布局即网格布局,是一种新的 CSS 布局模型,比较擅长将一个页面划分为几个主要区域,以及定义这些区域的大小、位置、层次等关系。号称是最强大的的 CSS 布局方案,是目前唯一一种 CSS 二维布局。利用 Grid 布局,我们可以轻松实现类似下图布局,演示地址 讲到布局,…icon-default.png?t=N7T8https://juejin.cn/post/6854573220306255880        

        我的选择

               Grid布局是最佳选择。因为二维布局提供的网格概念,使得元素的大小和位置可以被合理配置。但是Grid布局的兼容性很一般。考虑到有拖拽和放大缩小功能,且是基于Vue2的项目,我选择了一个网格(栅格)拖拽布局库vue-grid-layout 。他可以完美地实现自定义布局功能。

编辑器页面

          这是一个完整的编辑器页面。除开上部的公共导航栏之外。编辑器由左边的组件栏和右边的设计器组成。

        组件的设计器设计为弹窗模式。

        拖拽、移动的浮窗效果。

        dashboard-design.vue:编辑器外壳,通过插槽来插入页面模块

// dashboard-design.vue
<template>
  <div :class="$style.design">
    <global-header :class="$style.header">
      <template slot="center">
        <slot name="header-center" />
      </template>
    </global-header>
    <slot />
  </div>
</template>

        index.vue:仪表盘编辑器文件

// index.vue

<template>
  <dashboard-design-layout
    :id="$route.params.id"
  >
    // 头部
    <dashboard-design-header-center
      slot="header-center"
    />
    <template>
      <div
        id="dashboardContent"
      >
        // 左边的组件列表
        <a-card
          :class="$style.controls"
        >
          <control-list :dragType.sync="dragType" @add="handleClickAddField" />
        </a-card>
        // 右边设计器区域
        <a-card :class="$style.main">
         ....
        </a-card>
      </div>
  </dashboard-design-layout>
</template>

        此时一个简单的编辑机页面结构就出来了,紧接着我们需要给它添加CSS样式

设计器概览

 <a-card :class="$style.main">
    // 按钮
          <div :class="$style.action">
            <div :class="$style.actionLeft">
              <a-button type="link" @click="handlePreview">
                <x-icon type="tc-icon-search-square" />
                <span>预览</span>
              </a-button>
            </div>
            <div :class="$style.actionRight">
              <async-button type="primary" :click="validateAndSave">
                   保存
              </async-button>
            </div>
          </div>
    // 设计器
          <drag-container
            ref="container"
            :fields="fields"
            :dragType.sync="dragType"
            :selectedField="selectedField"
            :dashboard="form"
            :initFieldStyle="initFieldStyle"
            @add="handleAdd"
            @edit="handleEdit"
            @delete="handleDelete"
            @select="selectField"
          />
</a-card>

         我们关注drag-container.vue是怎么样的实现,它一定会有的两个功能:

        ① 能展示现有的设计布局

        ② 能自适应拖拽进来的元素,并允许用户自定义它的位置和大小

        让我们看看它的实现:

// drag-container.vue
<template>
  <div
    id="dashboardLayout"
    :style="styleCSSVariable"
  >
    <background :background="themeBackground">
        <grid-layout
          ref="layout"
          :class="$style.layout"
          :layout.sync="layout"
          :colNum="60"
          :rowHeight="15"
          @dragover.native="handleDrag"
          @dragleave.native="handleDragLeave"
          @drop.native="handleDrop"
        >
             ....
        </grid-layout>
    </background>
  </div>
</template>

        可以看到,根元素这里使用了一个名为styleCSSVariable的CSS集。这里的实现是:

get styleCSSVariable() {
    return createDashboardCSS(this.dashboard.setting.style);
}

        这里可以根据当前仪表盘的用户设置风格(如“暗黑”、“科技”、“酷炫”)进行css样式管理。return了诸如这些设置:

 return {
    // 文字颜色
    '--font': '#cccccc',
    '--font-sub': '#999999',
    '--font-info': '#666666',
    // 辅助文字
    '--font-color-secondary': '#aaaaaa',
    // 提示性文字
    '--font-color-info': '#ff2d2d',
    '--font-active-light': '#ffffff',

    // 删除按钮/文字
    '--delete': '#fe5959',
    // 警示色
    '--warning': '#ff9900',
    // 操作成功
    '--success': '#19be6b'
    ....
}

         可以看到设计器的元素style样式上绑定了一堆颜色定义。这其实就是CSS中的var()函数用法。详情可以看我这篇推文

CSS的var()函数用法与JS获取css函数变量值的方法_AI3D_WebEngineer的博客-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/weixin_42274805/article/details/133135688?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22133135688%22%2C%22source%22%3A%22weixin_42274805%22%7D        往下看,这里面套了个background的背景板。紧接着是我们的主角。来自于vue-grid-layout插件的grid-layout画布组件。

colNum:画布总共设计为60列

rowHeight:每行高度

:layout.sync="layout" 这里传入的是当前的布局信息

@dragover.native 这里响应的是原生的dragover 事件。拖拉到当前节点上方时,在当前节点上持续触发(相隔几百毫秒)

@dragleave.native 拖拉操作离开当前节点范围时,在当前节点上触发

@drop.native drag 被拖拉的节点或选中的文本,释放到目标节点时,在目标节点上触发

      这里为什么要设置三个事件呢?drop响应的是用户放手的事件,dragleave是用户手势离开设计器的时候响应,dragover是用户的手势在设计器里持续响应(刷新状态)。而我们知道,drag原生事件并不只三个:
        dragstart:用户开始拖拉时,在被拖拉的节点上触发。
        dragend:拖拉结束时(释放鼠标键或按下 ESC 键)在被拖拉的节点上触发。
        dragenter:拖拉进入当前节点时,在当前节点上触发一次,该事件的target属性是当前节点。

        但这些对于我们设计器来说并无作用。

设计器拖拽部分(一)

        大概明白了设计器的拖拽设计,紧接着我们来看最关键的数据结构。        ​​​

             其实很简单,我们只需要传入一个layout-item数组到layout-grid,它就可以正常响应渲染。哪怕数组是乱序的。也就是说layout-grid将严格按照每个layout-item的x/y/w/h进行计算布局

        layout-grid是不关注item的max/min-h/w属性的。因为这个事layout-item自己的事情。也就是说layout-grid甚至只是个容器。它接受的item数组是什么样,就会展示什么样。同时它响应上面的拖拽事件,方便我们生成每个layout-item数据。

        直接先看@drop.native="handleDrop"部分

  /** @name 拖动放置时 **/
  async handleDrop() {
    if (this.isInChildCom) return; // 进入子元素范围则无需触发
    try {
      let field = createDashboardField(this.dragType);
      field.widget.layout = pick(this.dragLayout, 'x', 'y', 'w', 'h');
      this.layout.splice(this.dragLayoutIndex, 1, {
        ...field.widget.layout,
        i: field.pkId,
      });
      this.$emit('add', field);
    } catch (e) {
      this.layout.splice(this.dragLayoutIndex, 1);
      throw e;
    }
  }
  /** @name 当前拖拽元素的layout **/
  dragLayout = null;
 /** @name 当前拖拽元素的index **/
  get dragLayoutIndex() {
    return this.layout.indexOf(this.dragLayout);
  }

        我把代码精简一下,大概是先创建一个拖拽组件的实例(谨慎地用try方法进行尝试,只要中途有抛错就把这个拖拽进来的元素切割掉)。紧接着更新layout。并把新增的组件实例抛出去(做数据存储)。

        请注意,this.layout仅仅用于当前页面layout-grid的渲染。不参与数据存储。

编辑器概览

        

             结合一下代码来看看:

// index.vue
<a-card>
    <!-- 左侧组件库-->
    <control-list :dragType.sync="dragType" @add="handleClickAddField" />
</a-card>
<a-card>
    <!-- 左侧设计器-->
   <drag-container
      :dragType.sync="dragType"
      :selectedField="selectedField"
    />
</a-card>

<script>
  ....
  /** @name 左边栏拖动时的组件类型 **/
  dragType = null;
</script>

        这里用了sync传值的语法糖。让子组件能够通过emit修改父组件的值。

// control-list.vue

<template>
  <div :class="$style.controlList">
     <div
        v-for="control in group.list"
          :key="control.type"
          @dragstart="handleDragStart(control.type, control.draggable)"
          @dragend="handleDragEnd(control.draggable)"
     >
        <x-icon :type="control.icon" />
        <span>{{control.name}}</span>
    </div>
 </div>
</template>

...

@Prop() dragType;

handleDragStart(type, draggable = true) {
  if (draggable) {
    this.$emit('update:dragType', type);
  }
}

handleDragEnd(draggable = true) {
  if (draggable) {
    this.$emit('update:dragType', null);
  }
}

    control-list.vue这里触发拖拽开始的事件就会emit一个有效的类型值给index.vue,如果拖拽停止就会emit一个空类型值给index。index再通过pro传参给drag-container,这里drag-container为什么也要用sync呢?因为当拖拽放置完成之后,drag-container会把类型值清空,以恢复起始的模式。

// drag-container.vue

<template>
    <grid-layout
          @dragover.native="handleDrag"
          @dragleave.native="handleDragLeave"
          @drop.native="handleDrop"
        >
          <grid-item
            v-for="layoutItem in layout"
            :key="layoutItem.i"
            v-bind="getLayoutProps(layoutItem)"
     
          >
            <dash-layout-item
                  @inChildComponent="inChildComponent"
            >
                  <component
                    :is="getComponent(layoutItem)"
                  />
            <dash-layout-item>/>
          </grid-item>
        </grid-layout>
</template>


@Prop() dragType;

         我们来看看这个dragType的作用是什么?

/** @name 拖动放置时 **/
async handleDrop() {
    ...
    try {
      let field = createDashboardField(this.dragType);
    } catch {
        ...
    } finally {
      this.$emit('update:dragType', null);
    }
}

        第一个作用,在拖拽放置的时候将组件类型传给创建方法。如果为null,则被捕获错误,回滚拖拽操作。不管成功与否,最后finally必须把父组件的dragType置为空以结束流程。

  /** @name 左边栏拖动触发 **/
  @Watch('dragType')
  handleDragTypeChange(type) {
    this.isInChildCom = false; // 重新拖动,重置是否在现有组件内部的判断
    if (type) {
      // 生成默认的布局item属性
      this.dragLayout = {
        i: 'drag',
        ...getDashboardLayoutByType(type),
      };
    } else {
      this.dragLayout = null;
    }
  }

        第二个作用,观察dragType值。并根据传入的组件类型为其生成默认的布局属性(x,y,w,h)之类。

        再来看看

<grid-item
   v-for="layoutItem in layout"
   :key="layoutItem.i"
   v-bind="getLayoutProps(layoutItem)" 
>
   <dash-layout-item
      @inChildComponent="inChildComponent"
   >
     <component :is="getComponent(layoutItem)"/>
  <dash-layout-item>/>
</grid-item>

        这里为什么要套个dash-layout-item,就是为了监控拖拽元素是否已经进入到现有元素。

// dash-layout-item
<template>
  <div
    @dragenter="dragenter"
    @dragleave="dragleave"
  >
    <slot />
  </div>
</template>

<script>

  /** @name 进入-有效目标 **/
dragenter() {
    this.$emit('inChildComponent', true);
}

  
 /** @name 离开-有效目标 **/
dragleave(e) {
    ...
    this.$emit('inChildComponent', false);
}

</script>

         

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值