需求背景
公司营销管理后台,工作台首页不支持可配置,页面布局写死了,现产品想实现工作台首页支持可配置。需要实现以下几点功能:
- 添加卡片:包含我的项目、我完成的、我的待办、我创建的、我完成的、今日统计、我的审批等卡片
- 可实现自由拖拽卡片进行位置布局&保存更新首页布局
- 恢复默认工作台布局
实现效果图
代码结构目录
基本使用
引入
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下报错的目录
vue3引入vue-grid-layout报错
解决办法: yarn add vue-grid-layout@3.0.0-beta1
import VueGridLayout from 'vue-grid-layout'
createApp(App).use(VueGridLayout)
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;
核心代码
<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>