可拖拽工作台实战

782 阅读2分钟

需求背景

公司营销管理后台,工作台首页不支持可配置,页面布局写死了,现产品想实现工作台首页支持可配置。需要实现以下几点功能:

  1. 添加卡片:包含我的项目我完成的我的待办我创建的我完成的今日统计我的审批等卡片
  2. 可实现自由拖拽卡片进行位置布局&保存更新首页布局
  3. 恢复默认工作台布局

实现效果图

Gif_223.gif

代码结构目录

image.png

基本使用

引入

import ImsGridLayout from '@/components/ims-grid-layout/index.vue'
export default {
  components: {
    ImsGridLayout,
  },
}

使用

   <ims-grid-layout
      ref="gridLayout"
      :gridData="list"
      :isConfigGrid="isConfigGrid"
      :isConfigCardBtn.sync="isConfigCardBtn"
    ></ims-grid-layout>

属性

gridData

Type: Array
Required: false
Default: []

// example
{
  businessType: 1,
  button: '[]',
  buttons: [],
  cardCode: 'MyTaskList',
  cardId: '1438872418653044771',
  extField: '',
  h: 5,
  i: 1,
  id: '1529662881589579780',
  isExistence: '',
  minH: 5,
  minW: 6,
  moduleDesc: '',
  moduleDescEn: '',
  moreLink: '',
  name: '我的工作待办',
  nameEn: '我的工作待办',
  orderIndex: 1,
  path: '/card/my-task-list',
  status: null,
  tag: '',
  tenantId: '',
  thumbnail: '',
  thumbnailUrl: '1507623361088696322',
  w: 8,
  x: 0,
  y: 0,
  componentPath: 'src/components/card/MyTaskList.vue',
}

isConfigGrid

Type: Boolean
Required: true
Default: false

卡片是否可以拖拽

isConfigCardBtn

Type: Boolean
Required: true
Default: true 工作台配置卡片按钮是否显示

vue-grid-layout踩坑记录

npm ...扩展运算符编译报错

解决办法: 修改babel-loader配置;默认babel-loader是exclude node_modules目录的;但是旧的项目,不支持es6部分语法;所以要包含 node_modules下报错的目录 image.png image.png

vue3引入vue-grid-layout报错

解决办法: yarn add vue-grid-layout@3.0.0-beta1

import VueGridLayout from 'vue-grid-layout'
createApp(App).use(VueGridLayout)

image.png

linear-gradient实现vue-grid-layout方格背景

 background: #d2d2d2;
  background-image: linear-gradient(90deg, #f2f2f2 10px, transparent 0),
    linear-gradient(#f2f2f2 10px, transparent 0);
  background-size: calc(8.33333% - 0.8px) 110px;

image.png

核心代码

<template>
  <div class="ims-home">
    <grid-layout
      :class="{ 'vue-grid-layout-edit': isConfigGrid }"
      :layout.sync="layout"
      :col-num="12"
      :row-height="90"
      :is-draggable="isConfigGrid"
      :is-resizable="isConfigGrid"
      :is-mirrored="false"
      :vertical-compact="true"
      :margin="[16, 16]"
      :use-css-transforms="true"
    >
      <template v-for="item in layout">
        <grid-item
          :x="item.x"
          :y="item.y"
          :w="item.w"
          :h="item.h"
          :i="item.i"
          :key="item.i"
          :minW="item.minW || 4"
          :minH="item.minH || 4"
        >
          <div v-if="cardComponents[item.cardCode]" class="card-item">
            <div class="vue-grid-item-header">
              <div class="title">{{ item.name }}</div>
              <div class="setting" v-if="!isConfigGrid">
                <svg-icon
                  title="点击刷新此卡片"
                  class="card-icon icon-refresh"
                  name="refresh"
                  @click.native="handleRefresh(item)"
                ></svg-icon>
                <!-- <template v-for="(_item, _index) in item.buttons">
                  <i :key="_index + '_split'" class="icon-split"></i>
                  <i
                    :key="_index"
                    :class="[
                      _item.icon,
                      (_item.icon === item.activeIcon ||
                        (!item.activeIcon && _index === 0)) &&
                        'active',
                    ]"
                    :title="_item.name"
                    @click="execCardFunction(item, _item.icon, _item.func)"
                  ></i>
                </template> -->
                <i class="icon-split"></i>
                <svg-icon
                  v-if="isConfigCardBtn"
                  title="点击放大此卡片"
                  name="zoom-in"
                  class="card-icon"
                  @click.native="handleZoomIn(item)"
                ></svg-icon>
                <svg-icon
                  v-else
                  title="点击缩小此卡片"
                  class="card-icon"
                  name="zoom-out"
                  @click.native="handleZoomOut(item)"
                ></svg-icon>
              </div>
            </div>
            <div class="vue-grid-item-body">
              <component
                v-bind="{
                  w: item.w,
                  h: item.h,
                }"
                :is="cardComponents[item.cardCode]"
              ></component>
            </div>
          </div>
          <template v-else>
            {{ item.i }}
          </template>
          <svg-icon
            v-if="isConfigGrid"
            class="vue-grid-item-close"
            name="close"
            @click.native="handleRemove(item.i)"
          ></svg-icon>
        </grid-item>
      </template>
    </grid-layout>
  </div>
</template>

<script>
import { GridLayout, GridItem } from 'vue-grid-layout'
import _ from 'lodash'

export default {
  name: 'ims-grid-layout',
  components: {
    GridLayout: GridLayout,
    GridItem: GridItem,
  },
  props: {
    isConfigGrid: {
      type: Boolean,
      default: false,
      required: true,
    },
    isConfigCardBtn: {
      type: Boolean,
      default: true,
      required: true,
    },
    gridData: {
      type: Array,
      default: () => [],
    },
  },
  watch: {
    gridData(newVal) {
      this.layout = _.cloneDeep(newVal)
      this.renderComponent()
    },
  },
  computed: {
    existCardCode() {
      return this.layout.map((item) => item.cardCode)
    },
    latestCardIndex() {
      return _.last(this.layout)?.i
    },
  },
  data() {
    return {
      layout: _.cloneDeep(this.gridData),
      cardComponents: {},
    }
  },
  mounted() {
    this.renderComponent()
  },
  methods: {
    getComponentPath(path) {
      const strArr = path.split('components/')
      if (strArr && strArr.length) {
        return strArr[strArr.length - 1]
      }
      return path
    },
    // 渲染组件
    renderComponent() {
      const cardCodes = this.layout.filter((item) => item.cardCode)
      const resultComponent = (item) => {
        let result = null
        let pathStr = this.getComponentPath(item.componentPath)
        console.log(pathStr, 'path===')
        result = import(`@/components/${pathStr}`).catch(() =>
          import(`@/components/${pathStr}`).catch(() =>
            import(`../card/${item.cardCode}`)
          )
        )
        return result
      }
      cardCodes.forEach((item) => {
        this.cardComponents[item.cardCode] = () => {
          return resultComponent(item)
        }
      })
    },
    handleRemove(gridIndex) {
      this.layout = this.layout.filter(
        (item) => Number(item.i) !== Number(gridIndex)
      )
    },
    handleRefresh(item) {
      const cardCode = _.camelCase(item.cardCode)
      // myProject-refresh
      this.$bus.$emit(`${cardCode}-refresh`, item)
    },
    handleZoomIn(item) {
      const list = _.cloneDeep(this.layout)
      list.forEach((gridItem) => {
        gridItem.x = 0
        gridItem.y = 0
        gridItem.w = gridItem.i === item.i ? 12 : 0
      })
      this.layout = [...list]
      this.$emit('update:isConfigCardBtn', false)
    },
    handleZoomOut() {
      this.layout = [...this.gridData]
      this.$emit('update:isConfigCardBtn', true)
    },
    resetGrid() {
      this.layout = [...this.gridData]
    },
  },
}
</script>

<style lang="scss" scoped>
.ims-home {
  padding-top: 56px;
  .vue-grid-layout-edit {
    background: #e5e6e8;
    background-image: linear-gradient(90deg, #f2f3f4 16px, transparent 0),
      linear-gradient(#f2f3f4 16px, transparent 0);
    background-size: calc(8.33333% - 1.69px) 106px;
    .vue-grid-item {
      border-radius: 0;
    }
  }
  .vue-grid-item {
    background: #ffffff;
    border-radius: 12px;
    box-shadow: 0 1px 4px 0 rgba(39, 45, 54, 0.1);
    .vue-grid-item-close {
      right: -5px;
      top: -5px;
      position: absolute;
      cursor: pointer;
      color: #cccccc;
      font-size: 20px;
    }
  }
  .card-item {
    height: 100%;
    overflow: hidden;
  }
  .vue-grid-item-header {
    display: flex;
    line-height: 20px;
    padding: 24px 24px 8px 24px;
    .title {
      flex: 1;
      color: rgba(0, 0, 0, 0.85);
      font-size: 16px;
      text-align: left;
    }
    .setting {
      display: flex;
      justify-content: flex-end;
      align-items: center;
      width: 30%;
      text-align: right;
      color: rgba(0, 0, 0, 0.45);
      font-size: 0;
      .card-icon {
        cursor: pointer;
        font-size: 16px;
        &:last-child {
          margin-right: 0;
        }
        &.active {
          color: #3d6af2;
        }
        &:hover {
          color: darken(#3d6af2, 20%);
        }
      }
      .icon-split {
        width: 1px;
        height: 8px;
        background: #f2f3f4;
        font-size: 0;
        display: inline-block;
        margin: 0 12px;
      }
    }
  }
  .vue-grid-item-body {
    padding: 0 24px 24px;
    height: calc(100% - 65px);
    overflow-y: hidden;
    &:hover {
      overflow-y: scroll;
      padding: 0 18px 24px 24px;
    }
  }
}
</style>

参考资料

vue动态组件&异步导入