前言
-
最近需要一个在平面图标注点位功能,去搜了一圈,发现......,最后在查阅文档一顿操作之后。
-
不停修改bug之后,做出一版可以基本使用的版本。
-
最后发现canvas标签可以完成很多功能,电子签名,点位标注,问题标注,画图功能等等
效果
canvas画布
功能难点
画布渲染问题-基于canvas提供的渲染方法封装渲染方法,x,y坐标,width,height高,url图片
移动距离问题-我们需要借助鼠标点击,移动,弹起来事件计算移动的距离,来更改x,y坐标
选中图标问题-当我们画布上有多个图标时通过x,y,加上width,height和点击时x,y坐标判断是那个图标在被点击,在数组中找到匹配返回下标,反之就是点击背景图
画布渲染问题-生成画布之后,为了让用户无感操作,最好以帧方式刷新画布(定时器方式)
图标数据格式-画布上图标有很多个图标,改变一个同时,其他也是要跟着渲染
设备信息问题-我们需要在画布上点击获取到图标的下标之后,把x,y传递给信息框,显示
自己理解
现在这个版本仅仅相当于是一个例子,但是也是费了不少时间和bug才艰辛完成的
为什么说是例子,可能还会有bug,适配,api交互,放大,存储问题等等。
代码实现-可以直接复制
<template>
<div class="app-container">
<!-- 侧边栏 -->
<el-row :gutter="20">
<!-- 树结构 -->
<el-col :xs="24" :sm="4" :lg="4">
<div class="grid-left">
<!-- 筛选框 -->
<el-input
class="searchinput"
placeholder="请搜索楼层"
suffix-icon="el-input__icon el-icon-search"
v-model="treeinput"
clearable
size="small"
></el-input>
<!-- 树结构 -->
<el-tree
:data="treedata"
:props="defaultProps"
@node-click="handleNodeClick"
ref="menuTree"
node-key="id"
default-expand-all
:filter-node-method="filterNode"
></el-tree>
</div>
</el-col>
<!-- 点位模块 -->
<el-col :xs="24" :sm="15" :lg="15">
<div class="grid-right">
<!-- 点位标题 -->
<div class="grid-top"></div>
<!-- 图片展示 -->
<img class="bigImg" :src="backpicture" v-if="backpicture" />
<!-- 生成画布模块 -->
<div class="canvas-box" v-show="this.canvasinit">
<!-- 表头 -->
<div class="canvas-title">
<div class="zuo">
请拖动图标到安装位置-<i>厂家平面图 平面图</i>
</div>
</div>
<!-- 画布 -->
<canvas
ref="canvas"
width="970"
height="500"
@mousedown="canvasMouseDown"
@mousemove="canvasMouseMove"
@mouseup="canvasMouseUp"
></canvas>
</div>
<!-- 保存平面图 -->
<div class="bottom" v-if="!this.canvasinit">
<el-button type="primary" @click="save">保存</el-button>
</div>
<!-- 图标提示信息 -->
<el-popover
placement="top"
id="popovercan"
width="200"
v-model="canvasvisible"
>
<div class="popover-top">
<i>传感器设备</i>
</div>
<p>序列号:sjhdkjshkj</p>
<p>设备类型:是给大家灰色轨迹</p>
<p></p>
</el-popover>
</div>
</el-col>
<!-- 设备信息 -->
<el-col :xs="24" :sm="5" :lg="5">
<div class="grid-table">
<!-- 标题 -->
<div class="grid-top">
<i></i>
<p>配置资源点-点击图标加载到画布中</p>
</div>
<!-- 表格数据 -->
<div class="newtable">
<div
class="new-item"
v-for="item in tableData"
:key="item.id"
@click="handleClick(item)"
>
<img :src="item.img" alt="" />
<div class="newconter">
<p>序列号码:{
{ item.phone }}</p>
<p>设备类型:{
{ item.newtype }}</p>
<p>详细位置:{
{ item.sys }}</p>
</div>
</div>
</div>
<div class="pagination">
<el-pagination
small
:page-size="pageInfo.pageSize"
layout="prev, pager, next"
:total="pageInfo.total"
>
</el-pagination>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
name: "Ceshi",
watch: {
treeinput(val) {
this.$refs.menuTree.filter(val);
},
},
data() {
return {
// 树结构筛选框
treeinput: "",
// 树结构数据
treedata: [
{
id: 1,
name: "中国",
children: [
{
id: 1,
name: "广东",
children: [
{
id: 4,
name: "惠州",
},
{
id: 5,
name: "深圳",
},
{
id: 6,
name: "广州",
},
],
},
{
id: 2,
name: "湖北",
children: [
{
id: 7,
name: "武汉",
},
],
},
{
id: 3,
name: "北京",
},
],
},
],
// 树结构配置
// 树形数据分析
defaultProps: {
id: "id",
label: "name",
children: "children",
},
// 获取canvas标签
canvas: null,
// 创建画布
ctx: null,
// 画布大小
canvasWidth: 970,
canvasHeight: 500,
//定时器
intervalId: null,
//判断鼠标是否点击
isClick: false,
//记录需要移动的图片的开光
index: -1,
frameNumber: 20,
sensorImgList: [],
backgroundImg: {
url: "https://img2.baidu.com/it/u=2832413337,2216208892&fm=253&fmt=auto&app=138&f=JPEG?w=544&h=500",
x: 0,
y: 0,
width: 970,
height: 500,
},
canvasSensorImg: [
{
channelId: 12,
height: 46,
url: "https://img2.baidu.com/it/u=3823882177,3352315913&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
width: 46,
x: 247,
y: 233,
},
{
channelId: 13,
height: 46,
url: "https://img2.baidu.com/it/u=3823882177,3352315913&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
width: 46,
x: 400,
y: 400,
},
],
tableData: [
{
id: 1,
img: "https://img2.baidu.com/it/u=3823882177,3352315913&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
phone: "865566043823044",
newtype: "NP-FDY100-N",
sys: "中国北京市北京人名大会堂侧门旁边",
},
],
// 图标数据
Icondata:
"https://img2.baidu.com/it/u=3823882177,3352315913&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
// 背景图图片
backpicture:
"https://img2.baidu.com/it/u=2832413337,2216208892&fm=253&fmt=auto&app=138&f=JPEG?w=544&h=500",
// 画布开关
canvasinit: false,
// 设备抱紧查询
pageInfo: {
// 总条数
total: 17,
// 当前页
pageNo: 1,
// 每页条数
pageSize: 10,
},
clickicon: {},
canvasvisible: false,
};
},
created() {},
methods: {
// 树结构点击事件
handleNodeClick(data) {
if (data.children.length !== 0) {
return;
}
console.log("树形结构", data);
console.log(data.id);
// 楼层id
this.page.floorId = data.id;
},
// 树节点搜索
filterNode(value, data) {
if (!value) return true;
return data.name.indexOf(value) !== -1;
},
// 确认保存按钮
save() {
this.canvasinit = true;
this.init();
},
// 创建画布
init() {
// 找到画布标签
this.canvas = this.$refs.canvas;
this.ctx = this.canvas.getContext("2d");
// 创建背景,图标,移动图标
this.loadBgImg();
// 刷新画布
this.dataRefreh();
},
loadBgImg() {
let img = new Image();
let bgImg = this.backgroundImg;
img.src = bgImg.url;
img.onload = () => {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.ctx.drawImage(img, bgImg.x, bgImg.y, bgImg.width, bgImg.height);
this.drawCanvasSensorImg();
this.loadSensorImg();
};
},
//加载图标
loadSensorImg() {
let imgList = [];
for (let i = 0; i < this.sensorImgList.length; i++) {
let img = new Image();
let sensorImg = this.sensorImgList[i];
img.src = sensorImg.url;
let imgs = {};
imgs.img = img;
imgs.x = sensorImg.x;
imgs.y = sensorImg.y;
imgs.width = sensorImg.width;
imgs.height = sensorImg.height;
// console.log(imgs)
imgList.push(imgs);
}
this.drawImg(imgList);
},
//绘制图片方法
drawImg(imgList) {
for (let i = 0; i < imgList.length; i++) {
this.ctx.drawImage(
imgList[i].img,
imgList[i].x,
imgList[i].y,
imgList[i].width,
imgList[i].height
);
}
},
// 绘制移动的图片
drawCanvasSensorImg() {
let imgList = [];
for (let i = 0; i < this.canvasSensorImg.length; i++) {
let img = new Image();
let sensorImg = this.canvasSensorImg[i];
img.src = sensorImg.url;
let imgs = {};
imgs.img = img;
imgs.x = sensorImg.x;
imgs.y = sensorImg.y;
imgs.width = sensorImg.width;
imgs.height = sensorImg.height;
imgList.push(imgs);
}
this.drawImg(imgList);
},
//判断鼠标是否在图标范围内,并返回下标
isMouseInIcon(e, imgList) {
let x = e.offsetX;
let y = e.offsetY;
for (let i = 0; i < imgList.length; i++) {
let imgX = imgList[i].x;
let imgY = imgList[i].y;
let imgWidth = imgList[i].width;
let imgHeight = imgList[i].height;
if (
x > imgX &&
x < imgX + imgWidth &&
y > imgY &&
y < imgY + imgHeight
) {
return i;
}
}
return -1;
},
// 定时器刷新画布
dataRefreh() {
if (this.intervalId != null) {
return;
}
this.intervalId = setInterval(() => {
this.loadBgImg();
}, this.frameNumber);
},
//鼠标点击触发事件
canvasMouseDown(e) {
console.log("鼠标点击", e);
this.isClick = true;
this.index = this.isMouseInIcon(e, this.canvasSensorImg);
if (this.index == -1) {
console.log("没选中");
return;
}
this.$nextTick(() => {
console.log("top");
const canpro = document.getElementById("popovercan");
canpro.style.position = "absolute";
canpro.style.top = this.canvasSensorImg[this.index].y + 40 + "px";
canpro.style.left = this.canvasSensorImg[this.index].x - 60 + "px";
this.canvasvisible = !this.canvasvisible;
});
},
//鼠标移动触发事件
canvasMouseMove(e) {
if (!this.isClick) {
return;
}
if (this.index != -1) {
this.canvasvisible = false;
let x = e.offsetX;
let y = e.offsetY;
this.canvasSensorImg[this.index].x =
this.canvasSensorImg[this.index].x < 0
? 0
: x - this.canvasSensorImg[this.index].width / 2;
this.canvasSensorImg[this.index].y =
this.canvasSensorImg[this.index].y < 0
? 0
: y - this.canvasSensorImg[this.index].height / 2;
}
},
//鼠标抬起触发事件
canvasMouseUp(e) {
console.log("执行了");
this.isClick = false;
},
handleClick(item) {
// 判断是否上传楼层图片
// 创建点位
let imgs = {};
imgs.url = this.Icondata;
imgs.x = 0;
imgs.y = 0;
imgs.width = 46;
imgs.height = 46;
// 加载点位图标
this.canvasSensorImg.push(imgs);
this.$message.success("请拖动图标到指定点位");
},
},
beforeDestroy() {
clearInterval(this.intervalId);
this.intervalId = null;
},
};
</script>
<style lang="scss" scoped>
.app-container {
height: 815px;
.grid-left {
padding: 30px 20px 20px;
min-height: 815px;
border: 1px solid #ccc;
// 输入框
.searchinput {
margin-bottom: 20px;
}
}
.grid-right {
// width: 1363px;
width: 100%;
height: 815px;
padding: 30px 20px 20px;
border: 1px solid #ccc;
position: relative;
// 标题
.grid-top {
width: 100%;
}
// 图片展示
.bigImg {
display: block;
width: 970px;
height: 500px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -54%);
}
// 画布模块
.canvas-box {
width: 1000px;
height: 620px;
padding: 10px;
background-color: #fff;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -55%);
cursor: pointer;
.canvas-title {
display: flex;
// background-color: skyblue;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
}
// 保存平面图
.bottom {
width: 95%;
height: 100px;
border-top: 1px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
// background-color: skyblue;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 0;
}
// 设备弹出框
::v-deep .el-popover {
// background-color: skyblue;
padding: 0;
.popover-top {
display: flex;
justify-content: space-between;
align-items: center;
.popover-title {
width: 30px;
height: 35px;
cursor: pointer;
background-color: #e72528;
display: flex;
justify-content: center;
align-items: center;
border-radius: 0 5px 0 0;
i {
font-size: 25px;
}
}
i {
font-style: normal;
// font-size: 20px;
}
}
p {
margin: 3px 0;
}
}
}
.grid-table {
padding: 30px 5px 5px;
height: 815px;
border: 1px solid #ccc;
.grid-top {
display: flex;
align-items: center;
// background-color: skyblue;
i {
display: block;
width: 3px;
height: 19px;
background-color: #409eff;
// border-radius: 2px;
margin-right: 5px;
}
p {
// font-size: 17px;
// font-weight: 700;
color: rgb(36, 37, 37);
}
}
// 设备列表
.newtable {
background-color: #fff;
.new-item {
display: flex;
justify-content: space-between;
align-items: center;
height: 80px;
padding: 0 5px;
cursor: pointer;
// background-color: skyblue;
border-top: 2px solid #c8c8c8;
// border-bottom: 2px solid #c8c8c8;
&:last-child {
border-bottom: 2px solid #c8c8c8;
}
img {
width: 45px;
height: 45px;
}
.newconter {
margin-left: 10px;
font-size: 12px;
p {
padding: 0;
margin: 2px 0;
}
}
}
}
.pagination {
margin-top: 10px;
display: flex;
justify-content: center;
align-content: center;
}
}
}
</style>
总结:
经过这一趟流程下来相信你也对 vue-使用canvas画布实现平面图点位标注功能 有了初步的深刻印象,但在实际开发中我 们遇到的情况肯定是不一样的,所以我们要理解它的原理,万变不离其宗。加油,打工人!
什么不足的地方请大家指出谢谢 -- 風过无痕