系统需要添加智能消防栓模块。集成了一家采用NbIOT通讯的智能消防栓产品。由第厂家平台对接NbIot特联网平台,我们平台提供一个api从第三方平台接收消防栓状态,用SignlaR把状态推送到前端。需要写一个状态实时同步界面,包括倾斜报警、盖子打开报警、出水告警等。
先看下测试效果图。
附Vue源码
<template>
<div style="color:white;height:100%;">
<div id="todetail">
<router-link :to="`/admin/stand/commondevice?sid=${$route.query.sid}`">{{this.$t("详情")}}</router-link>
</div>
<div class="detailview">
<div class="detailview_1">
<div>
<box style="position:relative">
<div id="centerTop" style="position:absolute;top:20px;left:20px">
<!-- <div style="font-size:1.5rem">{{this.$t("消防栓检测")}}</div> -->
<div style="font-size:1.5rem">测试</div>
<div>盖子打开<input v-model="animate.cap.angel" /> 盖子角度{{animate.cap.currentAngel}}</div>
<div> 栓柱倾斜<input v-model="animate.bolt.angel" /> 倾斜角度{{animate.bolt.currentAngel}}</div>
<div>
<b-form-checkbox switch size="lg" v-model="animate.water.action">放水</b-form-checkbox>
</div>
</div>
<div id="leftTop" style="width:100%;height:100%"></div>
</box>
</div>
</div>
<div class="detailview_1">
<div>
<box id="canvas_box" style="position:relative">
<div :style="canvasStyle">
<canvas ref="canvas_bolt" style="position:absolute;left:0;bottom:0;"></canvas>
<canvas ref="canvas_cap" style="position:absolute;left:0;bottom:0;"></canvas>
<canvas ref="canvas_water" style="position:absolute;left:0;bottom:0;"></canvas>
<canvas ref="canvas_wall" style="position:absolute;left:0;bottom:0;"></canvas>
</div>
</box>
</div>
</div>
<div class="detailview_1">
<div>
<box>
</box>
</div>
</div>
<div class="detailview_2">
<div>
<box> </box>
</div>
</div>
</div>
<img :src="boltData.pic" id="boltPic" @load="drawBolt(false)" hidden/>
<img :src="capData.pic" id="capPic" @load="drawCap(false)" hidden/>
<img :src="wallData.pic" id="wallPic" @load="drawWall" hidden/>
</div>
</template>
<script>
import box from "@/components/box.vue";
import alarm from "@/components/alarm";
export default {
components: { box, alarm },
data: function() {
return {
/*
动画参数配置
*/
animate: {
/*
盖子动画参数配置
*/
cap: {
thread: null,//保存定时器
angel: 0,//盖子角度
currentAngel: 0,//当前角度
speed: 20 //盖子角度变化的速度
},
bolt: {
thread: null,
angel: 0,
currentAngel: 0,
speed: 20
},
water: {
thread: null,
rate: 0,
speed: 0,
action: false
}
},
/**画布父元素的css */
canvasStyle: {
position: "absolute",
left: 0,
bottom: 0,
width: "100%",
height: "100%"
},
/**画布初始化参数 */
initData: {
/**原图尺寸 */
size: {
width: 340,
height: 380
},
/**渲染尺寸 */
rendSize: {
width: 0,
height: 0
}
},
/**底部墙渲染数据 */
wallData: {
//画片
pic: require("@/assets/img/hr/ic_di.png"),
//canvas上下文
ctx: null,
//原图尺寸
size: {
width: 340,
height: 166
},
//渲染尺寸
rendSize: {
width: 0,
height: 0
},
//起点坐标
startPos: {
x: 0,
y: 0
}
},
//栓柱渲染数据
boltData: {
pic: require("@/assets/img/hr/ic_xfx.png"), //图片
ctx: null,//canvas上下文
offsetX: 45,//X轴偏移量
RBX: 160,//栓柱右下角X轴偏移量
offsetRBX: 160,//栓柱右下角X轴渲染后偏移量
//原图尺寸
size: {
width: 127,
height: 216
},
//渲染尺寸
rendSize: {
width: 0,
height: 0
},
//起点坐标
startPos: {
x: 0,
y: 0
}
},
//盖子渲染数据
capData: {
pic: require("@/assets/img/hr/ic_xfx_gz.png"),//图片
ctx: null,//canvas上下文
offsetX: 172,//x轴偏移量
offsetY: 72,//y軕偏移量
//原图尺寸
size: {
width: 20,
height: 46
},
//渲染尺寸
rendSize: {
width: 0,
height: 0
},
//起点坐标
startPos: {
x: 0,
y: 0
}
},
//水滴渲染数据
waterData: {
ctx: null,//canvas上下文
//渲染尺寸
rendSize: {
width: 0,
height: 0
}
}
};
},
mounted: function() {
this.$nextTick(() => {
//初始化
this.initCanvas();
window.addEventListener("resize", () => {
//浏览器尺寸大小改变后重新渲染
this.initCanvas(true);
});
});
},
watch: {
//监控盖子的角度
"animate.cap.angel": function() {
this.startCapAnimate();
},
//监控栓柱的角度
"animate.bolt.angel": function() {
this.startBoltAnimate();
},
//监控是否放水
"animate.water.action": function() {
//把柱子立正
this.animate.bolt.currentAngel = 0;
this.animate.bolt.angel = 0;
//开盖子
this.animate.cap.currentAngel = 90;
this.animate.cap.angel = 90;
if (this.animate.water.action) {
//放水
this.startWater();
} else {
//停水
window.clearInterval(this.animate.water.thread);
this.waterData.ctx.clearRect(
0,
0,
this.initData.rendSize.width,
this.initData.rendSize.height
);
//关盖子
this.animate.cap.angel = 0;
}
}
},
methods: {
initCanvas(isDraw) {
//调整画布位置及大小
//父元素大小
var parentWidth = document.getElementById("canvas_box").clientWidth;
var parentHeight = document.getElementById("canvas_box").clientHeight;
//画布宽度最大值为父元素宽度
this.initData.rendSize.width =
this.initData.size.width > parentWidth
? parentWidth
: this.initData.size.width;
//根据比率设置画布高度
var rate = this.initData.size.width / this.initData.size.height;
this.initData.rendSize.height = this.initData.size.height / rate;
//画面高度最大值为父元素高度
if (this.initData.rendSize.height > parentHeight) {
this.initData.rendSize.height = parentHeight;
//再次调整宽度
this.initData.rendSize.width = this.initData.rendSize.height * rate;
}
//设置画布大小
this.canvasStyle.width = `${this.initData.rendSize.width}px`;
this.canvasStyle.height = `${this.initData.rendSize.height}`;
if (this.initData.rendSize.width < parentWidth) {
//画布居中
var diff = parentWidth - this.initData.rendSize.width;
this.canvasStyle.left = `${diff / 2}px`;
}
//设置最底部墙的渲染数据
var rateWall = this.wallData.size.width / this.wallData.size.height;
this.wallData.rendSize.width = this.initData.rendSize.width;
this.wallData.rendSize.height = this.initData.rendSize.width / rateWall;
this.wallData.startPos.x = 0;
this.wallData.startPos.y =
this.initData.rendSize.height - this.wallData.rendSize.height;
this.$refs.canvas_wall.width = this.initData.rendSize.width;
this.$refs.canvas_wall.height = this.initData.rendSize.height;
this.wallData.ctx = this.$refs.canvas_wall.getContext("2d");
//设置消防栓的渲染数据
var rateBolt = this.boltData.size.width / this.boltData.size.height;
var rateBoltWidth = this.boltData.size.width / this.initData.size.width;
this.boltData.rendSize.width =
this.initData.rendSize.width * rateBoltWidth;
this.boltData.rendSize.height = this.boltData.rendSize.width / rateBolt;
var rateBoltOffset =
this.initData.size.width / this.initData.rendSize.width;
this.boltData.startPos.x = this.boltData.offsetX / rateBoltOffset;
this.boltData.offsetRBX = this.boltData.RBX / rateBoltOffset;
this.boltData.startPos.y =
this.initData.rendSize.height -
(this.wallData.rendSize.height + this.boltData.rendSize.height);
this.$refs.canvas_bolt.width = this.initData.rendSize.width;
this.$refs.canvas_bolt.height = this.initData.rendSize.height;
this.boltData.ctx = this.$refs.canvas_bolt.getContext("2d");
//设置盖子的渲染数据
var rateCap = this.capData.size.width / this.capData.size.height;
var rateCapWidth = this.capData.size.width / this.initData.size.width;
this.capData.rendSize.width = this.initData.rendSize.width * rateCapWidth;
this.capData.rendSize.height = this.capData.rendSize.width / rateCap;
this.capData.startPos.x = this.capData.offsetX / rateBoltOffset;
this.capData.startPos.y =
this.capData.offsetY /
(this.boltData.size.height / this.boltData.rendSize.height) +
this.boltData.startPos.y;
this.$refs.canvas_cap.width = this.initData.rendSize.width;
this.$refs.canvas_cap.height = this.initData.rendSize.height;
this.capData.ctx = this.$refs.canvas_cap.getContext("2d");
//设置水滴渲染数据
this.$refs.canvas_water.width = this.waterData.rendSize.width = this.initData.rendSize.width;
this.$refs.canvas_water.height = this.waterData.rendSize.height = this.initData.rendSize.height;
this.waterData.ctx = this.$refs.canvas_water.getContext("2d");
//页面加载初始化不能画,因为可能图片还没有加载出来。
//浏览器大小调整时可以画,因为图片已经加载完毕
if (isDraw) {
this.drawWall();
this.drawBolt();
this.drawCap();
}
},
/*
画底部的墙
*/
drawWall() {
this.wallData.ctx.drawImage(
document.getElementById("wallPic"),
this.wallData.startPos.x,
this.wallData.startPos.y,
this.wallData.rendSize.width,
this.wallData.rendSize.height
);
},
/*
盏栓柱
angel:栓柱倾斜角度
*/
drawBolt(angel) {
angel = angel || this.animate.bolt.angel;
this.boltData.ctx.clearRect(
0,
0,
this.initData.rendSize.width,
this.initData.rendSize.height
);
this.boltData.ctx.save();
//以栓柱右下角为旋转基点
this.boltData.ctx.translate(
this.boltData.offsetRBX,
this.initData.rendSize.height - this.wallData.rendSize.height
);
this.boltData.ctx.rotate(angel * Math.PI / 180);
//以栓柱右下角为中心点,原来的起点坐标灰新坐标系中的位置
var cx = -(this.boltData.offsetRBX - this.boltData.startPos.x);
var cy = -this.boltData.rendSize.height;
this.boltData.ctx.drawImage(
document.getElementById("boltPic"),
cx,
cy,
this.boltData.rendSize.width,
this.boltData.rendSize.height
);
this.boltData.ctx.restore();
//第次还要调整一下盖子的位置,让盖子跟栓柱一起倾斜
this.drawCap(null, this.animate.bolt.currentAngel);
},
/*
画盖子
angel:盖子角度
boltAngel:栓柱角度,用于计算盖子的起点位置
*/
drawCap(angel, boltAngel) {
angel = angel || this.animate.cap.angel;
boltAngel = boltAngel || this.animate.bolt.angel;
this.capData.ctx.save();
//清空内容
this.capData.ctx.clearRect(
0,
0,
this.initData.rendSize.width,
this.initData.rendSize.height
);
//把控制点放到栓柱的右下角
this.capData.ctx.translate(
this.boltData.offsetRBX,
this.initData.rendSize.height - this.wallData.rendSize.height
);
//以栓柱右下角为旋转基点,旋转和栓柱一样的角度,这样不管栓柱倾斜到什么角度,盖子也能找到他的起点应该在的位置
this.capData.ctx.rotate(boltAngel * Math.PI / 180);
//旋转完后控制中心点坐标加到原来的位置
this.capData.ctx.translate(
-this.boltData.offsetRBX,
-this.initData.rendSize.height + this.wallData.rendSize.height
);
//再把中心点放点盖子的起点位置
this.capData.ctx.translate(
this.capData.startPos.x,
this.capData.startPos.y
);
//盖子的角度
var rotage = angel * Math.PI / 180 * -1;
this.capData.ctx.rotate(rotage);
this.capData.ctx.drawImage(
document.getElementById("capPic"),
0,
0,
this.capData.rendSize.width,
this.capData.rendSize.height
);
//画完后回到保存前的状态
this.capData.ctx.restore();
},
/*
栓柱倾斜效果
*/
startBoltAnimate() {
if (this.animate.bolt.thread) {
window.clearInterval(this.animate.bolt.thread);
}
this.animate.bolt.thread = window.setInterval(() => {
var exit = false;
//变化currentAngel,直到等于angel
if (this.animate.bolt.angel > this.animate.bolt.currentAngel) {
this.animate.bolt.currentAngel += 1;
if (this.animate.bolt.currentAngel > this.animate.bolt.angel) {
exit = true;
}
} else {
this.animate.bolt.currentAngel -= 1;
if (this.animate.bolt.currentAngel < this.animate.bolt.angel) {
exit = true;
}
}
if (exit) {
this.animate.bolt.currentAngel = this.animate.bolt.angel * 1;
}
//画栓柱,角度为currentAngel。每次角度累加或累减1
this.drawBolt(this.animate.bolt.currentAngel);
if (exit) {
window.clearInterval(this.animate.bolt.thread);
this.animate.bolt.thread = null;
}
}, this.animate.bolt.speed);
},
/*
盖子角度变化效果。
callBack:完成后的回调
*/
startCapAnimate(callBack) {
if (this.animate.cap.thread) {
window.clearInterval(this.animate.cap.thread);
}
this.animate.cap.thread = window.setInterval(() => {
var exit = false;
//变化currentAngel,直到等于angel
if (this.animate.cap.angel > this.animate.cap.currentAngel) {
this.animate.cap.currentAngel += 1;
if (this.animate.cap.currentAngel > this.animate.cap.angel) {
exit = true;
}
} else {
this.animate.cap.currentAngel -= 1;
if (this.animate.cap.currentAngel < this.animate.cap.angel) {
exit = true;
}
}
if (exit) {
this.animate.cap.currentAngel = this.animate.cap.angel * 1;
}
//画盖子,角度为currentAngel。每次角度累加或累减1
this.drawCap(this.animate.cap.currentAngel);
if (exit) {
window.clearInterval(this.animate.cap.thread);
this.animate.cap.thread = null;
if (callBack) {
callBack();
}
}
}, this.animate.cap.speed);
},
/*
开始放水
*/
startWater() {
if (this.animate.water.thread) {
window.clearInterval(this.animate.water.thread);
}
this.animate.water.thread = window.setInterval(() => {
//控制变量累加,最大值为1
this.animate.water.rate += 0.03;
if (this.animate.water.rate >= 1) {
this.animate.water.rate = 0;
}
//画水流
this.drawPullWater(this.animate.water.rate);
});
},
/*
画水流
rate:控制变量
*/
drawPullWater(rate) {
//先清除画布
this.waterData.ctx.clearRect(
0,
0,
this.initData.rendSize.width,
this.initData.rendSize.height
);
//填充水流颜色
this.waterData.ctx.strokeStyle = "rgba(2,180,245,0.8)";
this.waterData.ctx.fillStyle = "rgba(2,180,245,0.8)";
this.waterData.ctx.beginPath();
//一次画4个水滴,水滴从盖口按二次贝塞尔曲线到终点
for (var i = 0; i < 4; i++) {
//起点坐标,盖口,每个水滴往下移一点,免点重叠
var start = {
x: this.capData.startPos.x + this.capData.startPos.x * 0.05,
y: this.capData.startPos.y + this.capData.startPos.y * (i + 1) * 0.15
};
//终点坐标,画面右下角,每个水滴往下移一点,免点重叠
var end = {
x: this.initData.rendSize.width,
y: start.y + start.y * 0.5 + i * (start.y * 0.1)
};
//获取二次贝塞尔曲线控制点
var control = this.getSecControl(start.x, start.y, end.x, end.y);
//水滴的水平角度依次加5度,直线公式:y=5x-5;
var angel = 5 * i - 5;
//获取当前要画的位置
var point = this.getCurpoint(start, end, control, rate);
//画水滴
this.drawCur(
this.waterData.ctx,
60,
angel,
15 + 0.15 * rate * 100,
point.x,
point.y
);
}
this.waterData.ctx.closePath();
this.waterData.ctx.fill();
},
/*
根据贝塞尔公式获取在指定贝塞尔曲线上的点
start:曲线起点
end:曲线终点
c:曲线控制点
t:控制变量,为0时是起点,为1时是终点
*/
getCurpoint(start, end, c, t) {
var x =
(1 - t) * (1 - t) * start.x + 2 * t * (1 - t) * c.x + t * t * end.x;
var y =
(1 - t) * (1 - t) * start.y + 2 * t * (1 - t) * c.y + t * t * end.y;
return {
x: x,
y: y
};
},
/*
获取二次贝塞尔曲线的控制点,X坐票取中点,Y坐标取起点Y坐标稍下。
startx:起点x坐标
starty:起点y坐标
endx:终点x坐标
endy:终点y坐标
*/
getSecControl(startx, starty, endx, endy) {
return {
x: startx + (endx - startx) / 2,
y: starty - endy * 0.1
};
},
/*
画水滴,由二条三次贝塞尔曲线组成
ctx:Canvas Context
angel:夹角大小(水滴大小)
hangel:水平角度(水滴方向)
line:从起点到水滴终点的距离长度(像素))))),决定了水滴长度
startx:起点x坐标
starty:起点y坐标
*/
drawCur(ctx, angel, hangle, line, startx, starty) {
angel = angel * Math.PI / 180;
hangle = hangle * Math.PI / 180;
var p1 = [];
var p2 = [];
p1[0] = Math.cos(hangle) * line + startx;
p1[1] = Math.sin(hangle) * line + starty;
p2[0] = startx + Math.cos(angel + hangle) * line;
p2[1] = starty + Math.sin(angel + hangle) * line;
ctx.moveTo(startx, starty);
var p = {
p1: p1,
p2: p2
};
var c1 = [(startx + p.p1[0]) / 2, (starty + p.p1[1]) / 2];
var c2 = [(p.p1[0] + p.p2[0]) / 2, (p.p1[1] + p.p2[1]) / 2];
ctx.bezierCurveTo(c1[0], c1[1], p.p1[0], p.p1[1], c2[0], c2[1]);
var c3 = [(p.p2[0] + startx) / 2, (p.p2[1] + starty) / 2];
ctx.bezierCurveTo(p.p2[0], p.p2[1], c3[0], c3[1], startx, starty);
ctx.stroke();
}
}
};
</script>