前文:
一、思路
1.用一个数组 list 维护编辑器中的数据。
2.把组件拖拽到画布中时,使用 push() 方法将新的组件数据添加到 list
3.编辑器使用 v-for 指令遍历list,将每个组件逐个渲染到画布
二、关于自定义组件
1.图层类型有自定义组件
2.控制面板属性配置使用的是element-ui里的页签、颜色选择器、输入框、开关后面有需要可以自己添加,配置属性
一、拖动、移动 、放置、图层放大缩小实现
widget-list.vue
一个元素如果要设为可拖拽,必须给它添加一个 draggable 属性
1.拖的实现: draggable="true" H5给了一个属性可以拖拽,要配合
2.放置的实现@dragover.prevent @drop="onDrop"
dragstart 事件-也可以定义onmousedown事件,在拖拽刚开始时触发。它主要用于将拖拽的组件信息传递给画布。
drop 事件,在拖拽结束时触发。主要用于接收拖拽的组件信息。
<template>
<!-- 组件列表 -->
<!-- draggable="true" H5给了一个属性可以拖拽 -->
<div class="widget-list">
<div
v-for="widget in list"
:key="widget.type"
class="widget"
draggable="true"
@mousedown="(e )=> $emit('onWidgetMouseDown',e,widget)"
>
{{ widget.label }}
</div>
</div>
</template>
<script>
export default {
data() {
return {};
},
props:{
list:{
type:Array,
equired:true,
},
},
methods:{
}
};
</script>
<style scoped>
</style>
App.vue
3.组件在画布中移动
3-1.首先需要将画布设为相对定位 position: relative,然后将每个组件设为绝对定位 position: absolute。
3-2.这边使用的依赖vue-drag-resize,并没有使用移动和松开事件按下事件也可以改为dragstart事件效果一致:
3.dragstart事件主要是获取:记录组件当前的位置
4.传给依赖的x,y为:放置的距离左侧的距离-鼠标落下鼠标距离左侧的距离=当前box的x轴位置,如图所示:
<template>
<div id="app">
<!-- 图层列表 -->
<el-tabs v-model="activeName" class="sidebar">
<el-tab-pane label="图层列表" name="layer">
<!-- 图层列表 -->
<div class="layer" v-for="item in list" :key="item.id">
{{ item.label }}
</div>
</el-tab-pane>
<el-tab-pane label="组件列表" name="widget">
<Widget-List :list="widgetList" @onWidgetMouseDown="onWidgetMouseDown" />
</el-tab-pane>
</el-tabs>
<!-- 操作面板 -->
<div class="panel"
@dragover.prevent
@drop="onDrop" >
<Drageer v-for="(item, i) in list" :key="item.id" ref="widget" class="box" :x="item.x" :y="item.y" :z="item.z" :w="item.w" :h="item.h" >
<component :value="item.value" class="inner-widget" :is="item.component"
:styles="item.styles" />
</Drageer>
</div>
<!-- 样式配置区域 -->
<!-- <style-sider></style-sider> -->
<!-- 右键菜单 -->
<!-- <context-menu></context-menu> -->
<!-- 对齐线 纵向 -->
<!-- <div></div> -->
</div>
</template>
<script>
let currentId = 0; //id计数器
let widgetX = 0;//差值
let widgetY = 0;//差值
let currentWidget = null;//获取当前点击的图层数据
//出现在panel上的组件
import BarChart from '@/components/bar-chart'
import AreaChart from '@/components/area-chart'
import CustomText from '@/components/custom-text'
import CustomVideo from '@/components/custom-video'
import MianjiChart from '@/components/mianji-chart'
//左侧小组件列表
import WidgetList from '@/components/widget-list/'
//静态配置
import * as CONFIG from '@/components/constants/config'
export default {
name: 'App',
components: {
WidgetList,
BarChart,
AreaChart,
CustomText,
CustomVideo,
MianjiChart,
},
data(){
return{
activeName:'widget',
list:[],
widgetList: CONFIG.WIDGET_LTST,//组件的数据结构
}
},
methods:{
//放置组件
onDrop(e,i){
//放置的距离左侧的距离-鼠标落下鼠标距离左侧的距离=当前box的x轴位置
let x = e.offsetX - widgetX;
let y = e.offsetY - widgetY;
if (i !== undefined) {
x += this.list[i].x;
y += this.list[i].y;
}
const newItem = ({
id: currentId++,//key绑定id
x,
y,
z: !this.list.length ? 0 : Math.max(...this.list.map(item => item.z)) + 1,//因为一开是空 所以给个默认值0
...currentWidget.default,
// w:this.currentWidget.w,//盒子初始值宽
// h:this.currentWidget.h,
label: currentWidget.label,//文字
component: currentWidget.component, // 新增的组件名
type: currentWidget.type,//新增组件的类型
styles: currentWidget.styles,//新增组件的样式
});
this.list.push(newItem)
// this.clickDrageeronFoucus(newItem)
},
//在小组件鼠标落下的时候
onWidgetMouseDown(e, widget) {
//获取 鼠标距离左侧的距离
widgetX = e.offsetX;
widgetY = e.offsetY;
currentWidget = widget;//当前点击的图层数据赋值
},
}
}
</script>
<style>
body {
margin: 0;
}
#app {
display: flex;
width: 100vw;
height: 100vh;
/* 可视高度的多少1vh=视窗高度的1% */
}
.sidebar {
width: 200px;
background: #e9e9e9;
}
.panel {
flex: 1;
background: #f6f6f6;
position: relative;
/* 给相对定位主要是因为 要根据panel盒子的左上角来进行绝对定位*/
}
.widget {
width: 100px;
height: 100px;
outline: 1px solid red;
font-size: 24px;
text-align: center;
line-height: 100px;
margin: 24px;
}
.box {
/* width: 100px;
height: 100px; */
/* 插件自带宽高 */
/* outline: 1px solid blue; */
outline: 1px solid rgba(0, 0, 0, 0);
position: absolute;
}
.inner-widget {
width: 100%;
height: 100%;
}
.layer {
width: 100%;
height: 50px;
line-height: 50px;
background: #e9e9e9;
}
.layer:hover {
background: #fff;
}
.currentbgm {
background: #fff;
}
.sider {
width: 200px;
background: #e9e9e9;
}
.sider.right {
width: 300px;
}
.standard-line {
width: 2px;
height: 100%;
/* background: rgba(31, 29, 29, 1); */
border-left: 2px #0f0f0f dashed;
position: absolute;
left: 200px;
}
.standard-line.correnct {
/* background: red; */
border-left: 2px red dashed;
}
#frame {
position: absolute;
outline: 2px dashed red;
}
</style>
4.静态资源管理
4-1.constants下的config.js-组件列表的配置项
import * as dft from "./default"
import * as styleFromConfig from "./style-form-config"
//组件列表的配置项
export const WIDGET_LTST = [
{
type:'mianji-chart',
component:'mianji-chart',
label:'面积图',
//因为default后面数据会越来越多,所以单独给了一个js去存
// default:{
// w:300,
// h:200,
// value:[
// {
// name:'Mon',
// value:820,
// },
// {
// name:'Tue',
// value:932,
// },
// {
// name:'Tue',
// value:1000,
// }
// ]
// }
default:dft.MIANJI_CHART_DATA,
styles:dft.MIANJI_STYLE,
styleFrom:styleFromConfig.MIANJI_CHART,
},
{
type:'area-chart',
component:'area-chart',
label:'饼图',
default:dft.AREA_CHART_DATA,
styles:dft.AREA_STYLE,
styleFrom:styleFromConfig.AREA_CHART,
},
{
type:'bar-chart',
component:'bar-chart',
label:'柱状图',
default:dft.BAR_CHART_DATA,
styles:dft.BAR_STYLE,
styleFrom:styleFromConfig.BAR_CHART,
},
{
type:'text',
component:'custom-text',
label:'文字',
default:dft.CUSTOM_TEXT_DATA,
styles:dft.CUSTOM_TEXT_STYLE,
styleFrom:styleFromConfig.TEXT,
},
{
type:'video',
component:'custom-video',
label:'视频',
default:dft.CUSTOM_VIDEO_DATA,
styles:dft.CUSTOM_VIDEO_STYLE,
styleFrom:styleFromConfig.VIDEO,
}
];
4-2.constants下的default.js-组件列表的配置项的默认样式
// 面积图默认数据
export const MIANJI_CHART_DATA = {
w: 300,
h: 200,
value: [
{
name: 'Mon',
value: 820,
},
{
name: 'Tue',
value: 932,
},
{
name: 'Tue',
value: 1000,
}
]
}
//面积图默认样式
export const MIANJI_STYLE = {
areaColor:'#ff0000',
lineColor:'#0000ff',
xAxisVisible:true,
}
// 饼图默认数据
export const AREA_CHART_DATA = {
w: 300,
h: 300,
value: [
{
name: 'zhangsan',
value: 500,
},
{
name: 'lisi',
value: 620,
},
{
name: 'wangwu',
value: 360,
}
]
}
//饼图默认样式
export const AREA_STYLE = {
title:'标题',
subtitle:'副标题'
}
//柱状图数据
export const BAR_CHART_DATA = {
w:250,
h:300,
value:[
{
name:'Mon',
value:120,
},
{
name:'Tue',
value:200,
},
{
name:'Wed',
value:150,
},
{
name:'Thu',
value:80,
}
]
}
//柱状图默认样式
export const BAR_STYLE = {
areaColor:'#ff0000',
lineColor:'#0000ff',
xAxisVisible:true,
}
//文字数据
export const CUSTOM_TEXT_DATA = {
w:200,
h:50,
value:'hello world!'
}
//文字默认样式
export const CUSTOM_TEXT_STYLE = {
color:'#000000',
fontSize:'24',
}
//视频数据
export const CUSTOM_VIDEO_DATA = {
w:400,
h:300,
value:"https://cdn.theguardian.tv/webM/2015/07/20/150716YesMen_synd_768k_vp8.webm"
}
//视频默认样式
export const CUSTOM_VIDEO_STYLE = {
// 是否显示控制条
ctrlBarVisible:true,
}
4-3.constants下的style-form-config.js-组件列表的配置项的面板属性
//面积图
export const MIANJI_CHART = [
{
key: 'areaColor',
label: '区域颜色',
component: 'el-color-picker',
},
{
key: 'lineColor',
label: '折线颜色',
component: 'el-color-picker',
},
{
key: 'xAxisVisible',
label: '是否显示X轴',
component: 'el-switch',
}
]
//饼图
export const AREA_CHART = [
{
key: 'title',
label: '标题',
component: 'el-input',
},
{
key: 'subtitle',
label: '副标题',
component: 'el-input',
},
]
//柱状图
export const BAR_CHART = [
{
key: 'areaColor',
label: '柱条的颜色',
component: 'el-color-picker',
},
{
key: 'lineColor',
label: '背景颜色',
component: 'el-color-picker',
},
{
key: 'xAxisVisible',
label: '是否显示X轴',
component: 'el-switch',
}
]
//文字数据
export const TEXT = [
{
key: 'color',
label: '颜色',
// component: 'color-picker',
component: 'el-color-picker',
},
{
key: 'fontSize',
label: '字体大小',
component: 'el-input-number',
}
]
//视频
export const VIDEO = [
{
key: 'ctrlBarVisible',
label: '是否显示控制条',
component: 'el-switch',
},
]
5.封装的组件类型
5-1.custom-video.vue视频组件
<template>
<player class="custom-video" :options="options" />
</template>
<script>
import "video.js/dist/video-js.css";
import { videoPlayer } from "vue-video-player";
export default {
components: {
player: videoPlayer, //给组件重起名
},
props:{
value:{
type:String, //name的类型是字符串
},
styles:{
type:Object,
required:true,
}
},
computed:{
options(){
return {
controls:this.styles.ctrlBarVisible,
autoplay: 'muted', //设置自动播放
muted: true, // 关闭视频的声音通道(静音),属于逻辑属性
volume: 0.5, // 初始的音量
language: "en",
playbackRates: [0.7, 1.0, 1.5, 2.0],// 播放速率
sources: [
{
type: "video/mp4",// 播放格式
src: "https://cdn.theguardian.tv/webM/2015/07/20/150716YesMen_synd_768k_vp8.webm", // 播放源
// src: this.value, // 播放源
},
],
poster: "/static/images/author.gif", //当视频能够播放时,用于指定代表视频的图像的URL
// width: 500, // 视频框的宽度
// height: 570, // 视频框的高度
}
}
},
data() {
return {
};
},
mounted(){
},
};
</script>
<style scoped>
/* scoped是当前样式只在当前组件中生效 */
/* player组件跟当前组件没有关系 - 所以可以对样式进行穿透-样式才会到组件中去*/
::v-deep.custom-video>div{
width: 100%;
height: 100%;
}
</style>
5-2.area-chart.vue面积图表组件
<template>
<e-charts
:option="option"
autoresize
/>
</template>
<script>
export default {
props:{
value:{
type:Array, //name的类型是字符串
required:true, //name是必要的
},
styles:{
type:Object,
required:true,
}
},
computed:{
option(){
return {
title: {
text: this.styles.title,
subtext: this.styles.subtitle,
left: "center",
},
tooltip: {
trigger: "item",
},
legend: {
orient: "vertical",
left: "left",
},
series: [
{
name: "Access From",
type: "pie",
radius: "50%",
},
],
}
}
},
data() {
return {
};
},
};
</script>
<style scoped>
</style>
6.安装依赖
6-1.element-ui取用了el-tabs页签、颜色选择器、输入框、开关--全局注册
npm i element-ui -S
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
6-2.vue-drag-resize拖拽放大缩小-全局注册
npm i -s vue-drag-resize
import VueDragResize from 'vue-drag-resize'//拖拽组件
Vue.component('Drageer', VueDragResize)//全局注册拖拽组件,并重命名为Drageer
6-3.面积图表-全局注册
npm install echarts vue-echarts
import "echarts";
import ECharts from 'vue-echarts'
Vue.component('ECharts', ECharts)//注册
6-3.视频-//写组件里面
npm install vue-video-player --save
import { videoPlayer } from "vue-video-player";
player: videoPlayer, //给组件重起名-写在component里面别直接复制
6-4.npm i vue-context全局
import 'vue-context/dist/css/vue-context.css';//引入右击菜单
7.显示效果如图 -拖动、放置、放大缩小