效果图:
编辑
编辑
配置页面主体代码:
左边控件模块以及中间内容模块:
主要采用vue-drag-resize插件实现拖拽功能,本来是运用的grid-layout实现拖拽,运用更简单,但是需求又改了,可能需要每个控件在图片上,所以改用了vue-drag-resize。
vue-drag-resize循环layout数组,这个对象数组是我们所有控件的详细配置,将此对象数组记载的宽,高,x轴定位,y轴定位全同步在控件中。并在左边控件栏添加click事件 将每个控件的item信息添加到layout数组里,并在vue-drag-resize里添加mousedown事件(为什么要用mousedown,因为click事件在这里用有bug click事件会在拖拽之前)触发唤出右边配置模块
<template>
<el-container class="JNPF-Portal">
<el-aside width="250px" class="left-box">
<el-scrollbar class="aside-scrollbar">
<div class="components-list">
<div class="components-part">
<el-collapse>
<el-collapse-item>
<template slot="title">
<div class="eqpcomponents-title">设备控件</div>
</template>
<div class="list">
<div v-for="(item, index) in eqpList" :key="index" class="components-item" @click="addComponent(item)">
<div class="components-body">
<i class="icon-ym icon-ym-scheduleExample" />
{{ item.Name }}
</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
<div class="components-title">标签控件</div>
<div class="list">
<div class="components-item" @click="addComponent(viewItem)">
<div class="components-body">
<i class="icon-ym icon-ym-scheduleExample" />
{{ viewItem.title }}
</div>
</div>
<div class="components-item" @click="addComponent(videoItem)">
<div class="components-body">
<i class="icon-ym icon-ym-scheduleExample" />
{{ videoItem.title }}
</div>
</div>
<div class="components-item" @click="addComponent(textItem)">
<div class="components-body">
<i class="icon-ym icon-ym-scheduleExample" />
{{ textItem.title }}
</div>
</div>
</div>
</div>
</div>
</el-scrollbar>
</el-aside>
<el-main class="center-box">
<div class="action-bar">
<el-button icon="el-icon-video-play" type="text" @click="preview" size="medium">
预览</el-button>
<el-button class="delete-btn" icon="el-icon-delete" type="text" @click="empty" size="medium">清空</el-button>
</div>
<div ref="addheight" class="layout-area" :style="canvasInfo">
<vue-drag-resize v-for="item in layout" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i" :z="item.z"
:isResizable="true" @mousedown.native="handleClick(item)" :class="{ 'active-item': item.i === activeId }"
class="dragResize" @dragstop="changeData" @resizestop="changeData">
<img height="100%" :alt="item.title" width="100%" :src="baseUrl + item.EquPicture[0].url"
v-if="item.jnpfKey == 'img'" />
<p v-if="item.jnpfKey == 'img' && item.titleShow" style="text-align: center;font-size: 18px;">{{ item.title }}
</p>
<div height="100%" width="100%" v-if="item.jnpfKey == 'view'"
:style="[{ color: item.color }, { fontSize: item.fontSize }]">
{{ item.title }}:{{ item.points ? item.points : '请配置测点字段' }} {{ item.unit ? item.unit : '单位' }}
</div>
<div height="100%" width="100%" v-if="item.jnpfKey == 'text'"
:style="[{ color: item.color }, { fontSize: item.fontSize }]">
{{ item.title }}
</div>
<div v-if="item.jnpfKey == 'video'" class="video">
<i class="el-icon-video-camera-solid"></i>
</div>
<div class="mask"></div>
<span title="复制" class="drawing-item-copy" @click="addComponent(item)">
<i class="el-icon-copy-document"></i></span>
<span title="删除" class="drawing-item-delete" @click="handleRemoveItem(item.i)">
<i class="el-icon-delete"></i></span>
</vue-drag-resize>
<!-- 空白时显示 -->
<div v-show="!layout.length" class="empty-info">
<img :src="require('@/assets/images/emptyPortal.png')" alt="" class="empty-img">
</div>
</div>
</el-main>
<right-panel :active-data="activeData" :canvasInfo="canvasInfo" />
<Preview :visible.sync="previewVisible" :layout="layout" />
</el-container>
</template>
<script>
import VueDragResize from 'vue-drag-resize'
import { deepClone } from '@/utils'
import { getDeviceList } from '@/api/iotSupervision'
import Preview from './Preview'
import RightPanel from './RightPanel'
const defaultConf = {
layouyId: 100,
layout: []
}
export default {
name: 'JNPF-IotDesign',
props: ['conf'],
components: {
Preview,
RightPanel,
VueDragResize
},
data() {
return {
eqpList: [],
baseUrl: process.env.VUE_APP_BASE_API,
viewItem: {
jnpfKey: 'view',
title: '标签输入',
points: null,
unit: null,
eqp: null,
color: '#888',
fontSize: '18px',
w: 250,
h: 40,
z: 10,
minW: 6,
minH: 4,
maxW: 12,
maxH: 6
},
textItem: {
jnpfKey: 'text',
title: '文本输入',
color: '#888',
fontSize: '18px',
w: 250,
h: 40,
z: 10,
minW: 6,
minH: 4,
maxW: 12,
maxH: 6
},
videoItem: {
jnpfKey: 'video',
title: '视频控件',
w: 320,
h: 200,
z: 15,
src: '',
},
viewStyle: {},
layout: [],
eqplayout: [],
activeId: null,
activeData: null,
previewVisible: false,
config: {},
changeDataId: 1,
canvasInfo: {
width: '1000px',
height: '750px'
},
}
},
watch: {
layout: {
handler(val) {
if (val.length === 0) this.config.layouyId = 100
},
deep: true
},
},
created() {
this.init()
},
mounted() {
if (typeof this.conf === 'object' && this.conf !== null) {
this.config = this.conf
} else {
this.config = deepClone(defaultConf)
this.config.layouyId = 100
}
this.layout = this.config.layout || []
},
methods: {
init() {
getDeviceList().then(res => {
res.data[0].children.forEach(item => {
console.log(item, '999');
item.EquPicture = JSON.parse(item.EquPicture)
item.icon = 'icon-ym icon-ym-generator-notice',
item.jnpfKey = 'img',
item.title = item.Name,
item.titleShow = true,
item.w = 200,
item.h = 200,
item.z = 5,
item.minW = 4,
item.minH = 7,
item.maxW = 12,
item.maxH = 7
})
this.eqpList = res.data[0].children
})
},
getData() {
return new Promise((resolve, reject) => {
this.config.layout = this.layout
this.config.canvasInfo = this.canvasInfo
resolve({ formData: this.config, target: 1 })
})
},
addComponent(item) {
let clone = deepClone(item)
let x = 0, y = 0, i = this.config.layouyId
if (this.layout.length) {
let lastItem = this.layout[this.layout.length - 1]
y = lastItem.y + lastItem.h
}
let row = { ...clone, i, x, y }
this.layout.push(row)
this.activeId = this.config.layouyId
this.activeData = row
this.config.layouyId++
},
handleRemoveItem(i) {
this.layout = this.layout.filter(item => item.i !== i);
this.activeId = null
this.activeData = {}
},
empty() {
this.$confirm('确定要清空所有吗?', '提示', { type: 'warning' }).then(() => {
this.layout = []
this.config.layouyId = 100
this.activeId = null
this.activeData = {}
}).catch(() => { })
},
preview() {
this.previewVisible = true
},
handleClick(item) {
this.activeId = item.i
this.activeData = item
},
resizedEvent(i) {
this.$refs['eChart' + i] && this.$refs['eChart' + i][0] && this.$refs['eChart' + i][0].chart && this.$refs['eChart' + i][0].chart.resize()
},
changeData(data) {
this.$set(this.activeData, 'x', data.left)
this.$set(this.activeData, 'y', data.top)
this.$set(this.activeData, 'w', data.width)
this.$set(this.activeData, 'h', data.height)
},
addheight() {
const num = parseInt(window.getComputedStyle(this.$refs.addheight).height)
this.$refs.addheight.style.height = num + 50 + 'px'
}
}
}
</script>
右边配置栏:
配置栏就没什么需要注意的 灵活运用watch和this.$set就行
<template>
<el-aside width="300px" class="right-box">
<div class="cap-wrapper">组件属性</div>
<el-scrollbar class="aside-scrollbar">
<el-form size="small" label-width="80px" labelPosition="left" style="padding-left: 10px;padding-top: 10px;">
<template v-if="activeData">
<el-form-item v-if="activeData.title !== undefined" label="标题">
<el-input v-model="activeData.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item v-if="activeData.jnpfKey === 'view'" label="测点字段">
<el-input v-model="activeData.points"></el-input>
</el-form-item>
<el-form-item v-if="activeData.jnpfKey === 'view'" label="设备编码">
<el-input v-model="activeData.eqp" placeholder="请输入设备编码" />
<!-- <el-select v-model="activeData.eqp" placeholder="请选择">
<el-option v-for="item in eqplayout" :key="item.id" :label="item.Name" :value="item.id">
</el-option>
</el-select> -->
</el-form-item>
<el-form-item v-if="activeData.jnpfKey === 'view'" label="单位">
<el-select v-model="activeData.unit" clearable placeholder="请选择">
<el-option v-for="item in unitList" :key="item.id" :label="item.fullName" :value="item.fullName">
</el-option>
</el-select>
</el-form-item>
<el-form-item v-if="activeData.jnpfKey === 'view' || activeData.jnpfKey === 'text'" label="字体颜色">
<el-color-picker v-model="activeData.color"></el-color-picker>
</el-form-item>
<el-form-item v-if="activeData.jnpfKey === 'view' || activeData.jnpfKey === 'text'" label="字体大小">
<el-input-number v-model="fontSize"></el-input-number>
</el-form-item>
<el-form-item v-if="activeData.jnpfKey === 'video'" label="网址路径">
<el-input v-model="activeData.src" placeholder="请输入src" />
</el-form-item>
<el-form-item v-if="activeData.jnpfKey === 'img'" label="是否显示">
<el-switch v-model="activeData.titleShow" />
</el-form-item>
<el-form-item label="宽">
<el-input-number v-model="activeData.w"></el-input-number>
</el-form-item>
<el-form-item label="高">
<el-input-number v-model="activeData.h"></el-input-number>
</el-form-item>
<el-form-item label="x">
<el-input-number v-model="activeData.x"></el-input-number>
</el-form-item>
<el-form-item label="y">
<el-input-number v-model="activeData.y"></el-input-number>
</el-form-item>
<el-form-item label="层级">
<el-input-number v-model="activeData.z"></el-input-number>
</el-form-item>
</template>
</el-form>
</el-scrollbar>
<div class="cap-wrapper">画布参数</div>
<el-scrollbar class="aside-scrollbar">
<el-form size="small" label-width="80px" labelPosition="left" style="padding-left: 10px;padding-top: 10px;">
<el-form-item label="画布宽度">
<el-input-number v-model="width"></el-input-number>
</el-form-item>
<el-form-item label="画布高度">
<el-input-number v-model="height"></el-input-number>
</el-form-item>
</el-form>
</el-scrollbar>
</el-aside>
</template>
<script>
import draggable from 'vuedraggable'
import { getSelectorAll } from '@/api/system/menu'
import { getDataInterfaceSelector } from '@/api/systemData/dataInterface'
import InterfaceDialog from '@/components/Process/PropPanel/InterfaceDialog'
import { getUnitList } from '@/api/IotDesign'
export default {
props: ['activeData', 'canvasInfo'],
components: { draggable, InterfaceDialog },
data() {
return {
areaVisible: false,
currentIndex: 0,
menuList: [],
dataInterfaceOptions: [],
unitList: [],
fontSize: 0,
width: 0,
height: 0,
}
},
created() {
this.getMenuList()
this.getDataInterfaceSelector()
this.getUnitList()
},
watch: {
'activeData.color': function (newVal, oldVal) {
this.$set(this.activeData, 'color', newVal)
},
'activeData.fontSize': function (newVal, oldVal) {
console.log('123123');
this.fontSize = parseInt(this.activeData.fontSize)
},
'canvasInfo.width': {
handler(newVal) {
this.width = parseInt(this.canvasInfo.width)
},
immediate: true,
// todo
},
'canvasInfo.height': {
handler(newVal) {
this.height = parseInt(this.canvasInfo.height)
},
immediate: true,
// todo
},
fontSize: {
handler(newVal) {
this.$set(this.activeData, 'fontSize', newVal + 'px')
},
},
width: {
handler(newVal) {
this.$set(this.canvasInfo, 'width', newVal + 'px')
},
},
height: {
handler(newVal) {
this.$set(this.canvasInfo, 'height', newVal + 'px')
},
},
},
methods: {
// 获取单位
async getUnitList() {
const res = await getUnitList()
this.unitList = res.data.list
},
getMenuList() {
getSelectorAll({ category: 'Web' }).then(res => {
this.menuList = res.data.list
})
},
getDataInterfaceSelector() {
getDataInterfaceSelector().then(res => {
this.dataInterfaceOptions = res.data
})
},
getSelectValue(data, i) {
if (!data[0]) {
this.$set(this.activeData.list, i, {
fullName: '',
id: '',
urlAddress: '',
icon: '',
iconBackgroundColor: '',
type: '',
propertyJson: '',
linkTarget: '_self',
enCode: ''
})
} else {
let iconBackgroundColor = ''
if (data[1].propertyJson) {
let propertyJson = JSON.parse(data[1].propertyJson)
iconBackgroundColor = propertyJson.iconBackgroundColor || ''
}
this.$set(this.activeData.list, i, {
fullName: data[1].fullName,
id: data[1].id,
urlAddress: data[1].urlAddress,
type: data[1].type,
propertyJson: data[1].propertyJson,
linkTarget: data[1].linkTarget,
enCode: data[1].enCode,
icon: data[1].icon,
iconBackgroundColor: iconBackgroundColor
})
}
},
addSelectItem() {
this.activeData.list.push({
fullName: '',
id: '',
urlAddress: '',
icon: '',
iconBackgroundColor: '',
type: '',
propertyJson: '',
linkTarget: '_self',
enCode: ''
})
},
delSelectItem(index) {
if (this.activeData.list.length < 3) {
this.$message({
message: '选项最少要保留两项',
type: 'warning'
});
return
}
this.activeData.list.splice(index, 1)
},
addDataBoardItem() {
this.activeData.list.push({ fullName: "", num: '', dataType: 'static', propsApi: '', icon: "" })
},
delDataBoardItem(index) {
if (this.activeData.list.length < 3) {
this.$message({
message: '选项最少要保留两项',
type: 'warning'
});
return
}
this.activeData.list.splice(index, 1)
},
openIconsDialog(index) {
this.iconsVisible = true
this.currentIndex = index
},
showData(option) {
this.areaVisible = true
this.$nextTick(() => {
this.$refs.JSONArea.init(option)
})
},
updateOption(data) {
let option = data ? JSON.parse(data) : {}
this.activeData.option = option
},
dataTypeChange() {
this.activeData.propsApi = ''
this.activeData.propsName = ''
},
propsUrlChange(data, index) {
if (!data || !data.length) {
this.activeData.list[index].propsApi = ''
this.activeData.list[index].propsName = ''
return
}
this.activeData.list[index].propsApi = data[0]
this.activeData.list[index].propsName = data[1].fullName
},
propsApiChange(val, item) {
if (!val) {
this.activeData.propsApi = ''
this.activeData.propsName = ''
return
}
this.activeData.propsApi = val
this.activeData.propsName = item.fullName
}
}
}
</script>
展示页面代码:
基本就和配置页面中间内容模块一样 设置成禁止拖拽 禁止缩放就行
区别是获取存在后端的json字符串进行赋值配置
其中业务逻辑多是处理了测点获取接口数据的项目需求 (功能需求要求的 可以不看)
<template>
<div class="layout-area red" :style="canvasInfo">
<vue-drag-resize v-for="item in layout" :key="item.i" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i"
:z="item.z" :isActive="false" :isResizable="false" :isDraggable="false" class="dragResize">
<img height="100%" :alt="item.title" width="100%" :src="baseUrl + item.EquPicture[0].url"
v-if="item.jnpfKey == 'img'" />
<p v-if="item.jnpfKey == 'img' && item.titleShow" style="text-align: center;font-size: 18px;">{{ item.title }}</p>
<div height="100%" width="100%" v-if="item.jnpfKey == 'view'"
:style="[{ color: item.color }, { fontSize: item.fontSize }]">
{{ item.title }}:{{ item.dataPoints ? item.dataPoints : '请配置测点字段' }} {{ item.unit ? item.unit : '' }}
</div>
<div height="100%" width="100%" v-if="item.jnpfKey == 'text'"
:style="[{ color: item.color }, { fontSize: item.fontSize }]">
{{ item.title }}
</div>
<div v-if="item.jnpfKey == 'video'" class="video" @click="videoDialogFn(item)">
<i class="el-icon-video-camera-solid"></i>
</div>
</vue-drag-resize>
<!-- 空白时显示 -->
<div v-show="!layout.length" class="empty-info">
<img :src="require('@/assets/images/emptyPortal.png')" alt="" class="empty-img">
</div>
<el-dialog :title="title" :visible.sync="videoDialog" width="60%" @close="videoDialogClose">
<div class="flex-f">
<video width="70%" height="70%" id="myVideo" preload="auto" muted type="rtmp/flv"></video>
</div>
</el-dialog>
</div>
</template>
<script>
import VueDragResize from 'vue-drag-resize'
import { getEqpPointsAll } from '@/api/IotDesign'
import flvjs from 'flv.js'
export default {
props: ["layout", 'type', 'canvasInfo'],
components: {
VueDragResize
},
data() {
return {
baseUrl: process.env.VUE_APP_BASE_API,
videoDialog: false,
videoSrc: '',
title: '',
};
},
activated() { },
watch: {
layout: {
deep: true,
handler(v) {
this.setData()
},
player: null,
}
},
created() {
// this.setData()
// console.log(this.datas.data[0].values['1-PowerAlarm'],'7878787');
},
mounted() {
// this.$nextTick(() => {
// this.createdPlay()
// })
},
methods: {
setData() {
// 标签数组
let arr = this.layout.filter(item => {
return item.jnpfKey == 'view'
})
// 集合转化为参数
// let newArr = []
// arr.forEach(item => {
// let obj = {}
// console.log(!newArr.length,'777');
// if (!newArr.length) {
// obj.eqpCode = item.eqp
// obj.points = [item.points]
// newArr.push(obj)
// } else {
// newArr.forEach(it => {
// if (item.eqp == it.eqpCode) {
// it.points.push(item.points)
// } else {
// obj.eqpCode = item.eqp
// obj.points = [item.points]
// newArr.push(obj)
// }
// })
// }
// }),
let newArr = arr.reduce((acc, cur) => {
let found = false
for (let i = 0; i < acc.length; i++) {
if (acc[i].eqpCode === cur.eqp) {
acc[i].points.push(cur.points)
found = true
break
}
}
if (!found) {
acc.push({
eqpCode: cur.eqp,
points: [cur.points]
})
}
return acc
}, [])
getEqpPointsAll(newArr).then(({ data }) => {
this.layout.forEach(item => {
data.forEach(it => {
if (item.eqp == it.eqpCode) {
Object.keys(it.values).forEach(i => {
if (item.points == i) {
item.dataPoints = it.values[i]
}
this.$forceUpdate()
})
}
})
})
})
},
videoDialogFn(item) {
this.videoDialog = true
// console.log(item, '999');
// this.videoSrc = item.src
this.title = item.title
this.createdPlay(item.src)
},
</script>
整个做下来还是挺有意思的 也遇到了很多bug 其中代码还可以封装得更好 比如每个控件的配置可以封装成js文件 vue-drag-resize中的控件显示也可以封装成额外的组件。
欢迎讨论