需求添加背景图,然后在图上画警戒线,某条警戒线报警时要闪烁,可添加标签文本
canvas画线,记录线起点终点坐标,线宽颜色、报警时线的颜色,然后根据坐标回显各条线段,用一字段判断某线段是否闪烁,然后定时器不断清空整张画布不断重绘
标签文本采用div,实现标签的添加、拖拽移动、修改样式和文本、删除、旋转
标签旋转原来用余弦定理,但效果不行,不知道是计算有误还是怎滴,后发现网友使用mousemove的e.clientY减去mouseup的e.clientY就得到了角度deg
需要注意的是mousemove是绑定在容器还是小div上,效果和体验是不一样的
地图绘制
<template>
<div class="app-container">
<div class="e-map">
<img :src="temp.a" class="map-bg" alt="">
<canvas id="baseCanvas"></canvas>
<canvas id="canvas"
:style="'cursor:' + (draw ? 'crosshair':'not-allowed') + ';z-index:' + (draw ? 3 : 2)"></canvas>
<div class="font-container"></div>
</div>
<div class="e-side">
<div class="block">
<div class="title">电子地图功能开关</div>
<el-checkbox v-model="temp.f">打开电子地图功能</el-checkbox>
</div>
<div class="block block-1">
<div class="title">电子地图设计</div>
<div class="btn-container">
<el-button type="primary" @click="handleAddImg">添加地图</el-button>
<el-button type="primary" :disabled="canClick" @click="handleDelImg">删除地图</el-button>
</div>
</div>
<div class="block block-2">
<div class="title">电子地图画防区线</div>
<el-form label-width="160px">
<el-form-item label="请选择防区">
<el-select v-model="temp.b">
<el-option label="防区A" :value="1"></el-option>
<el-option label="防区B" :value="1"></el-option>
<el-option label="防区C" :value="1"></el-option>
</el-select>
</el-form-item>
<el-form-item label="请选择画笔颜色">
<el-color-picker v-model="temp.c"></el-color-picker>
</el-form-item>
<el-form-item label="请选择画笔宽度">
<el-input-number v-model="temp.d" v-number-input controls-position="right"
:min="1"></el-input-number>
</el-form-item>
<el-form-item label="请选择报警时线条颜色">
<el-color-picker v-model="temp.e"></el-color-picker>
</el-form-item>
</el-form>
<div class="btn-container">
<el-button type="primary" :disabled="canClick" @click="drawLine">画防区线</el-button>
<el-button type="primary" :disabled="canClick" @click="revokeLine">删除防区线</el-button>
<el-button type="primary" :disabled="canClick" @click="writeText">画防标签</el-button>
<el-button type="primary" :disabled="canClick" @click="emptyConfig">清空所有配置</el-button>
</div>
</div>
</div>
<el-dialog title="标签属性"
custom-class="font-dialog"
:visible.sync="fontTempVisible"
width="420px">
<el-form ref="fontTemp" :model="fontTemp" :rules="fontRules" label-width="100px">
<el-form-item label="标签文本" prop="text">
<el-input type="textarea" v-model="fontTemp.text" placeholder="请输入文本"></el-input>
</el-form-item>
<el-form-item label="字体大小">
<el-input-number v-model="fontTemp.fontSize" v-number-input controls-position="right"
:min="12"></el-input-number>
</el-form-item>
<el-form-item label="字体颜色">
<el-color-picker v-model="fontTemp.fontColor"></el-color-picker>
</el-form-item>
<el-form-item label="文本字体">
<el-select v-model="fontTemp.fontFamily">
<el-option v-for="item in fontFamilyList" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
<el-form-item label="边框宽度">
<el-input-number v-model="fontTemp.borderWidth" v-number-input controls-position="right"
:min="1"></el-input-number>
</el-form-item>
<el-form-item label="边框颜色">
<el-color-picker v-model="fontTemp.borderColor"></el-color-picker>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="fontTempVisible = false">取 消</el-button>
<el-button type="primary" @click="fontCreate">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import $ from 'jquery'
export default {
name: "e-map",
data() {
return {
temp: {
a: '',
b: '',
c: '#67C23A',
d: 2,
e: '#F56C6C',
f: false
},
baseCanvas: null,
draw: false, // 是否能绘制,用于cursor样式
lineRecord: [], // 线记录
fontRecord: [], // 字体记录
fontTempVisible: false,
fontFamilyList: ['SimSun', 'SimHei', 'Microsoft Yahei', 'KaiTi', 'NSimSun', 'FangSong'],
fontTemp: {},
fontRules: {
text: [{required: true, message: '请输入标签文本', trigger: 'blur'}]
}
}
},
computed: {
// 一些按钮在有地图时才可点击
canClick() {
return !this.temp.a
}
},
created() {
},
mounted() {
let $eMap = $('.e-map'),
$canvas = $('#canvas'),
$baseCanvas = $('#baseCanvas')
$canvas[0].width = $eMap.width()
$canvas[0].height = $eMap.height()
$baseCanvas[0].width = $eMap.width()
$baseCanvas[0].height = $eMap.height()
},
methods: {
fontCreate() {
this.$refs.fontTemp.validate(valid => {
if (valid) {
let id = this.fontTemp.id,
$font = $('#font' + id),
index = this.fontRecord.findIndex(v => v.id === id)
$font.find('.text').text(this.fontTemp.text)
$font.css({
fontSize: this.fontTemp.fontSize,
color: this.fontTemp.fontColor,
fontFamily: this.fontTemp.fontFamily,
borderWidth: this.fontTemp.borderWidth,
borderColor: this.fontTemp.borderColor
})
this.fontRecord.splice(index, 1, this.fontTemp)
this.fontTempVisible = false
}
})
},
// 画防标签
writeText() {
let $map = $('.e-map'),
$fontContainer = $('.font-container'),
$canvas = $('#canvas'),
otherLeft = $map.offset().left,
otherTop = $map.offset().top
$canvas.off()
$map.off()
this.draw = true
$map.on('click', (e) => {
let x = e.clientX - otherLeft,
y = e.clientY - otherTop,
fontData = {
x,
y,
id: this.getRandomId(),
text: '',
fontSize: 14,
fontColor: '#000',
fontFamily: 'SimSun',
borderWidth: 1,
borderColor: '#000',
rotate: 0
},
$fontDiv = $(`<div class="label-block" id="font${fontData.id}" style="top:${y}px;left:${x}px">
<div class="text">请输入文本</div>
<span class="handle-remove el-icon-close"></span>
<span class="handle-edit el-icon-edit-outline"></span>
<span class="handle-rotate el-icon-refresh-left"></span>
</div>`)
// 文本鼠标拖拽
$fontDiv.on('mousedown', (e) => {
let mapWidth = $map.width(),
mapHeight = $map.height(),
diffX = e.clientX - $fontDiv.offset().left,
diffY = e.clientY - $fontDiv.offset().top,
fontDivWidth = $fontDiv.innerWidth(),
fontDivHeight = $fontDiv.innerHeight(),
x = 0, // 文本坐标
y = 0
$fontDiv.on('mousemove', (e) => {
x = e.clientX - otherLeft - diffX
y = e.clientY - otherTop - diffY
// 边界
if (x <= 0) {
x = 0
}
if (x >= mapWidth - fontDivWidth) {
x = mapWidth - fontDivWidth
}
if (y <= 0) {
y = 0
}
if (y >= mapHeight - fontDivHeight) {
y = mapHeight - fontDivHeight
}
$fontDiv.css({left: x, top: y})
})
$fontDiv.on('mouseup', () => {
fontData.x = x
fontData.y = y
$fontDiv.off('mousemove')
})
})
// 文本点击
$fontDiv.find('.handle-edit').on('click', () => {
this.fontTemp = fontData
this.fontTempVisible = true
this.$nextTick(() => this.$refs.fontTemp.clearValidate())
return false
})
// 删除文本
$fontDiv.find('.handle-remove').on('click', () => {
let $font = $('#font' + fontData.id),
index = this.fontRecord.findIndex(v => v.id === fontData.id)
$font.remove()
this.fontRecord.splice(index, 1)
})
// 文本旋转
$fontDiv.find('.handle-rotate').on('mousedown', (e) => {
let oldY = e.clientY,
oldX = e.clientX,
deg = 0
$map.on('mousemove', (e) => {
deg = e.clientY - oldY
$fontDiv.css({transform: `rotateZ(${deg}deg)`})
})
$map.on('mouseup', () => {
fontData.rotate = deg
$map.off('mousemove')
})
return false
})
$fontContainer.append($fontDiv)
$map.off()
this.fontRecord.push(fontData)
this.draw = false
})
},
// 清除所有防区线
revokeLine() {
this.baseCanvas.clearRect(0, 0, this.baseCanvas.canvas.width, this.baseCanvas.canvas.height)
},
// 清空所有配置
emptyConfig() {
this.$confirm('确定清空所有配置吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}).then(() => {
this.temp = {
a: '',
b: '',
c: '#67C23A',
d: 2,
e: '#F56C6C',
f: false
}
this.baseCanvas.clearRect(0, 0, this.baseCanvas.canvas.width, this.baseCanvas.canvas.height)
$('.font-container').empty()
}).catch(() => {
})
},
drawLine() {
let $canvas = $('#canvas'),
$baseCanvas = $('#baseCanvas'),
ctx = $canvas[0].getContext('2d'),
otherLeft = $canvas.offset().left,
otherTop = $canvas.offset().top
this.draw = true
if (this.baseCanvas === null) {
this.baseCanvas = $baseCanvas[0].getContext('2d')
}
$canvas.off()
// 画笔颜色
ctx.strokeStyle = this.temp.c
// 画笔大小
ctx.lineWidth = this.temp.d
$canvas.on('mousedown', (e) => {
let startX = e.clientX,
startY = e.clientY,
lineData = {
id: this.getRandomId(),
startPoint: {x: startX - otherLeft, y: startY - otherTop},
endPoint: {},
color: this.temp.c,
alarmColor: this.temp.e,
lineWidth: this.temp.d
}
$canvas.on('mousemove', (e) => {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
ctx.beginPath()
ctx.moveTo(startX - otherLeft, startY - otherTop)
ctx.lineTo(e.clientX - otherLeft, e.clientY - otherTop)
ctx.closePath()
ctx.stroke()
})
$canvas.on('mouseup', (e) => {
this.baseCanvas.drawImage($canvas[0], 0, 0, ctx.canvas.width, ctx.canvas.height)
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
lineData.endPoint.x = e.clientX - otherLeft
lineData.endPoint.y = e.clientY - otherTop
this.lineRecord.push(lineData)
$canvas.off()
this.draw = false
})
})
},
// 添加地图
handleAddImg() {
let input = document.createElement('input')
input.type = 'file'
input.accept = '.jpg,.png,.gif,.svg,.webp,.bmp'
input.click()
input.addEventListener('change', () => {
let fd = new FileReader(),
loading = this.$loading({
lock: true,
text: '地图加载中',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
})
fd.readAsDataURL(input.files[0])
fd.onload = (e) => {
loading.close()
this.temp.a = e.target.result
}
})
},
// 删除地图
handleDelImg() {
this.$confirm('确定删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}).then(() => {
this.temp.a = ''
}).catch(() => {
})
},
getRandomId() {
return Math.round(Math.random() * 1000000)
}
},
}
</script>
<style lang="scss" scoped>
.app-container {
height: calc(100vh - 50px);
position: relative;
overflow: auto;
}
.e-map {
1000px;
height: 800px;
border: 1px solid #DCDFE6;
position: absolute;
top: 20px;
left: 18px;
user-select: none;
.map-bg {
100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
#canvas,
#baseCanvas {
position: absolute;
top: 0;
left: 0;
z-index: 2;
cursor: not-allowed;
}
::v-deep .label-block {
border: 1px solid #000;
position: absolute;
padding: 4px 10px;
font-size: 14px;
cursor: move;
z-index: 2;
color: #000;
font-family: 'SimSun';
span {
position: absolute;
bottom: -18px;
right: 3px;
background: rgba(0, 0, 0, .4);
color: #fff;
font-size: 14px;
padding: 2px;
border-radius: 0 4px 4px 4px;
cursor: pointer;
display: none;
}
.handle-rotate {
right: -18px;
}
.handle-remove {
right: 24px
}
&:hover {
span {
display: block;
}
}
&::before {
content: "";
150%;
height: 280%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.font-container {
100%;
height: 100%;
}
}
.e-side {
margin-left: 1010px;
min- 350px;
.block {
border: 1px solid #DCDFE6;
padding: 25px 15px 15px;
100%;
position: relative;
margin: 5px 0 20px;
.el-select,
.el-input-number {
100%;
}
.title {
position: absolute;
background-color: #fff;
padding: 0 10px;
top: -9px;
left: 10px;
font-size: 14px;
font-weight: 700;
}
}
.btn-container {
display: grid;
grid-template-columns: 50% 50%;
.el-button {
margin: 10px 5px 0;
}
}
}
::v-deep .font-dialog {
.el-textarea,
.el-select,
.el-input-number {
250px;
}
.el-textarea {
textarea {
height: 60px;
resize: none;
}
}
.el-dialog__body {
padding: 20px 20px 0;
}
}
</style>
回显、警报
<template>
<el-dialog title="电子地图运行查看"
width="1400px"
@close="handleClose"
:visible="eMapVisible">
<div class="e-map-global">
<img :src="temp.a" class="map-bg" alt="">
<canvas id="baseCanvasGlobal"></canvas>
<div class="font-container"></div>
</div>
<div class="e-side">
<div class="block">
<div class="title">电子地图查看</div>
<el-select v-model="currentMap" @change="changeMap">
<el-option v-for="item in eMap" :label="item.name" :value="item"></el-option>
</el-select>
</div>
<div class="block">
<div class="title">防区控制</div>
<el-form label-width="80px">
<el-form-item label="控制范围">
<el-select v-model="temp.x">
<el-option label="当前地图" :value="1"></el-option>
<el-option label="全部地图" :value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<div>
<el-button type="primary">布防</el-button>
<el-button type="primary" @click="removeDefense">撤防</el-button>
<el-button type="primary" @click="resetStatus">复位</el-button>
</div>
</el-form-item>
</el-form>
<el-table :data="alarmList"
@row-click="handleClickRow"
:row-class-name="currentRow"
style="margin:70px 0 0">
<el-table-column label="报警防区" prop="name"></el-table-column>
<el-table-column label="报警时间" prop="time" width="135px"></el-table-column>
<el-table-column label="状态" width="60">
<span slot-scope="{ row }">{{ row.status | statusFilter }}</span>
</el-table-column>
</el-table>
<pagination :total="total" v-show="total > 0"
:page.sync="listQuery.page"
:background="false"
:small="true"
:limit.sync="listQuery.size"
layout="total, prev, pager, next"
@pagination="getList"></pagination>
</div>
</div>
</el-dialog>
</template>
<script>
import $ from 'jquery'
import {mapGetters} from 'vuex'
import Pagination from '@/components/Pagination'
export default {
name: "EMap",
components: {Pagination},
data() {
return {
currentMap: {},
eMap: [],
total: 0,
listQuery: {
page: 1,
size: 10
},
list: [],
temp: {
z: '',
x: 1,
a: '',
b: '',
c: '#67C23A',
d: 2,
e: '#F56C6C',
f: false
},
baseCanvas: null,
timeId: null,
openAlarm: false // 用于线段闪烁
}
},
computed: {
...mapGetters([
'alarmList'
]),
eMapVisible() {
return this.$store.state.app.eMapVisible
}
},
watch: {
eMapVisible(bool) {
if (bool) {
this.$nextTick(() => {
let $eMap = $('.e-map-global'),
$baseCanvas = $('#baseCanvasGlobal')
$baseCanvas[0].width = $eMap.width()
$baseCanvas[0].height = $eMap.height()
this.total = 1
this.changeMap(this.alarmList[0])
})
} else {
clearInterval(this.timeId)
}
}
},
methods: {
// 撤防
removeDefense() {
this.resetMap()
this.currentMap.alarm = false
this.currentMap.status = 1
this.currentMap.lineData.forEach(v => v.alarm = false)
this.openAlarm = false
this.drawMapSector(this.currentMap.lineData, false)
this.$message.success('撤防成功')
},
// 复位
resetStatus() {
this.resetMap()
this.currentMap.alarm = false
this.currentMap.status = 1
this.currentMap.lineData.forEach(v => v.alarm = false)
this.openAlarm = false
this.drawMapSector(this.currentMap.lineData, false)
this.$message.success('复位成功')
},
// 当前行样式
currentRow({row}) {
if (this.currentMap.id === row.id) {
return 'current-row'
}
},
// 绘制标签
drawFont(arr) {
let $container = $('.e-map-global .font-container')
arr.forEach(v => {
let $label = $(`<div class="label-block" id="font${v.id}">${v.text}</div>`)
$label.css({
left: v.x,
top: v.y,
color: v.color,
fontSize: v.fontSize,
fontFamily: v.fontFamily,
borderWidth: v.borderWidth,
borderColor: v.borderColor,
transform: `rotate(${v.rotate}deg)`
})
$container.append($label)
})
},
getList() {
},
handleClickRow(row) {
this.currentMap = row
this.resetMap()
this.temp.a = row.backgroundImage
this.drawMapSector(row.lineData)
// 若该地图警报,则闪烁对应线段
if (row.alarm) {
this.setMapAlarming(row.lineData)
}
// 标签重绘
$('.font-container').empty()
this.drawFont(row.fontData)
},
// 重置地图
resetMap() {
clearInterval(this.timeId)
this.clearCanvas()
},
// 警报响起
setMapAlarming(lineData) {
this.timeId = setInterval(() => {
this.openAlarm = !this.openAlarm
this.clearCanvas()
this.drawMapSector(lineData, this.openAlarm)
}, 300)
},
// 根据坐标画线,alarm用于线段闪烁
drawLine(param, alarm) {
if (this.baseCanvas === null) {
let $baseCanvas = $('#baseCanvasGlobal')
this.baseCanvas = $baseCanvas[0].getContext('2d')
}
this.baseCanvas.beginPath()
if (param.alarm) {
this.baseCanvas.strokeStyle = alarm ? param.alarmColor : param.color
} else {
this.baseCanvas.strokeStyle = param.color
}
this.baseCanvas.lineWidth = param.lineWidth
this.baseCanvas.moveTo(param.startPoint.x, param.startPoint.y)
this.baseCanvas.lineTo(param.endPoint.x, param.endPoint.y)
this.baseCanvas.stroke()
},
// 绘制防区所有线段
drawMapSector(lineData, alarm) {
lineData.forEach(v => {
this.drawLine(v, alarm)
})
},
changeMap(row) {
this.currentMap = row
this.temp.a = row.backgroundImage
this.drawMapSector(row.lineData)
// 开始警报
if (row.alarm) {
this.setMapAlarming(row.lineData)
}
// 绘制标签文本
this.drawFont(row.fontData)
},
clearCanvas() {
this.baseCanvas.clearRect(0, 0, this.baseCanvas.canvas.width, this.baseCanvas.canvas.height)
},
handleClose() {
this.$store.dispatch('app/toggleMapVisible', false)
}
},
filters: {
statusFilter(status) {
return ['未处理', '已处理'][status]
}
}
}
</script>
<style lang="scss" scoped>
::v-deep .el-dialog {
margin-top: 5vh !important;
.el-dialog__header {
padding: 7px 20px 10px;
.el-dialog__title {
font-size: 14px;
}
.el-dialog__headerbtn {
top: 12px;
}
}
.el-dialog__body {
padding: 0 20px;
height: 810px;
}
.pagination-container {
padding: 38px 0 0;
}
}
.e-map-global {
1000px;
height: 800px;
border: 1px solid #DCDFE6;
position: absolute;
top: 40px;
left: 18px;
.map-bg {
100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
#baseCanvasGlobal {
position: absolute;
top: 0;
left: 0;
z-index: 2;
}
.font-container {
100%;
height: 100%;
position: relative;
::v-deep .label-block {
border: 1px solid #000;
position: absolute;
padding: 4px 10px;
font-size: 14px;
z-index: 2;
color: #000;
font-family: 'SimSun';
transform-origin: center;
}
}
}
.e-side {
margin-left: 1010px;
min- 350px;
.block {
border: 1px solid #DCDFE6;
padding: 25px 15px 15px;
100%;
position: relative;
margin: 5px 0 20px;
.el-select,
.el-input-number {
100%;
}
.title {
position: absolute;
background-color: #fff;
padding: 0 10px;
top: -9px;
left: 10px;
font-size: 14px;
font-weight: 700;
}
}
.btn-container {
display: grid;
grid-template-columns: 50% 50%;
.el-button {
margin: 10px 5px 0;
}
}
}
</style>
数据
[{
id: 184841, // 前台生成的线id
name: '防区A',
time: '2020/12/15 13:20:11',
alarm: true, // 地图是否警报
status: 0, // 0未处理,1已处理
backgroundImage: require('@/assets/banner6.jpg'), // 背景图
lineData: [
{
id: 111431, // 前台生成的线id
alarmColor: '#f56c6c', // 警报时线颜色
color: '#67c23a', // 线颜色
lineWidth: 2, // 线宽
alarm: false, // 该线是否警报
startPoint: { // 线起点
x: 261,
y: 607
},
endPoint: { // 线终点
x: 500,
y: 102
}
},
{
id: 762733,
alarmColor: '#f56c6c',
color: '#ea190a',
lineWidth: 4,
alarm: false,
startPoint: {
x: 501,
y: 102
},
endPoint: {
x: 727,
y: 612
}
},
{
id: 381409,
alarmColor: '#f56c6c',
color: '#1d11f4',
lineWidth: 6,
alarm: false,
startPoint: {
x: 727,
y: 612
},
endPoint: {
x: 203,
y: 237
}
},
{
id: 906240,
alarmColor: '#f56c6c',
color: '#d00de1',
lineWidth: 8,
alarm: true,
startPoint: {
x: 202,
y: 236
},
endPoint: {
x: 789,
y: 237
}
},
{
id: 296668,
alarmColor: '#f56c6c',
color: '#edf109',
lineWidth: 10,
alarm: true,
startPoint: {
x: 789,
y: 238
},
endPoint: {
x: 262,
y: 607
}
},
],
fontData: [
{
id: 152964, // 前台生成的标签id
x: 220,
y: 300,
text: '天青色等烟雨',
fontSize: 20,
rotate: 45,
color: '#f56c6c',
fontFamily: 'SimHei',
borderWidth: 2,
borderColor: '#f56c6c'
}
]
}
]