基于vue3+leaflet的告警系统GIS一张图案例,覆盖功能点包括底图切换、告警事件模拟统计、模拟实时告警点移动信息、模拟告警管理以及数据诊断等,适合学习leaflet与前端框架结合开发webgis开发可视化项目。
demo源码运行环境以及配置
运行环境:依赖Node安装环境,demo本地Node版本:推荐v18+。
运行工具:vscode或者其他工具。
前端项目配置方式:下载demo源码,vscode打开,然后顺序执行以下命令:
(1)下载demo环境依赖包命令:npm install
(2)启动demo命令:npm run dev
(3)打包demo命令: npm run build
技术栈
Vue 3.2.39
Vite 2.5.10
leaflet 1.9.4
示例效果
核心源码
import { onMounted, onUnmounted, nextTick, ref, reactive } from "vue";
import L from "leaflet";
import * as echarts from "echarts";
import config from "../config";
import { getPointAlongLine, getDistanceBy2Points } from "../utils/geoUtils";
// import { useRouter } from "vue-router";
// import { ElMessage } from 'element-plus';
// const router = useRouter();
const isShowAlarmSystem = ref(false);
const isShowDataSystem = ref(false);
const isShowLeftMenu = ref(true);
// 告警事件统计图表对象
let myChartEvent = null;
// 诊断数据统计图表对象
let dataChartEvent = null;
// 按照日期切换统计图表
const radio = ref(1);
const alarmEventTal = ref({
emergency: 100,
important: 85,
general: 200,
prompt: 96
})
// 统计图表数据源-月
let echartsDataMonth = {
"xAxis": [
"雷击",
"外力破坏",
"覆冰",
"温度异常"
],
"yAxis": [
"154",
"125",
"188",
"178",
]
};
// 统计图表数据源-周
let echartsDataWeek = {
"xAxis": [
"雷击",
"外力破坏",
"覆冰",
"温度异常"
],
"yAxis": [
"64",
"75",
"58",
"88",
]
};
// 统计图表数据源-天
let echartsDataDay = {
"xAxis": [
"雷击",
"外力破坏",
"覆冰",
"温度异常"
],
"yAxis": [
"34",
"45",
"48",
"58",
]
};
// 数据诊断图表数据源
let timeXData = [
'0.00', '4.09', '8.17', '12.26', '16.35', '20.43', '24.52', '28.60', '32.69', '36.78', '40.86', '44.95', '49.04', '53.12', '57.21', '61.30', '65.38', '69.47', '73.55', '77.64', '81.73', '85.81',
'89.90', '93.99', '98.07', '102.16', '106.25', '110.33', '114.42', '118.50', '122.59', '126.68', '130.76', '134.85', '138.94', '143.02', '147.11', '151.20', '155.28', '159.37', '163.45', '167.54', '171.63', '175.71', '179.80', '183.89',
'187.97', '192.06', '196.15', '200.23', '204.32', '208.40', '212.49', '216.58', '220.66', '224.75', '228.84', '232.92', '237.01', '241.10', '245.18', '249.27', '253.35', '257.44', '261.53', '265.61', '269.70', '273.79', '277.87', '281.96'
];
let timeYData = [
13.996026, 25.939534, 24.403596, 24.156721, 22.940715, 20.067240, 12.103183, 11.610609, 10.647344, 11.052902, 12.320346, 12.305370, 19.471151, 18.599405, 28.122542, 28.465133, 28.932984, 29.017473, 38.485951, 38.036698, 38.076452, 47.790099, 47.922228, 37.787734,
37.816976, 37.440732, 27.890553, 27.412264, 27.708192, 27.559679, 27.617277, 17.692056, 17.692056, 17.692056, 17.368156, 27.278625, 27.414416, 37.849099, 17.414209, 16.960530, 16.960530, 17.411365, 17.665097, 27.495684, 27.103351, 27.506305, 27.238517, 27.154806, 27.296673, 17.110990, 17.104874, 17.445841,
17.249234, 17.576882, 18.122866, 17.470368, 17.534492, 17.575769, 7.834490, 7.624720, 7.938114, 7.337769, 7.783019, 7.611189, 7.107963, 7.622926, 8.084923, 7.655780, 7.464569, 7.790213
]
const formData = ref({
id: '',
data_type: '',
channel_number: '',
})
// 数据诊断面板
let dataDialogVisible = ref(false);
// 告警管理面板
let alarmDialogVisible = ref(false);
const formAlarmData = reactive({
id: '',
alarmType: '',
siteName: '',
timeDate: '',
shortcuts: [
{
text: '最近一周',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
return [start, end]
},
},
{
text: '最近一个月',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
return [start, end]
},
},
{
text: '最近3个月',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
return [start, end]
},
},
]
})
// 告警管理的实时告警信息表
const tableData = [
{
eventCode: '1',
siteName: 'xx桩',
mileageLocation: 'xx桩+0.34km',
lon: 114.320941,
lat: 34.799099,
opticalCableDistance: '3.34km',
peak: '50℃',
alarmType: '高温异常',
alarmLevel: '红色预警',
alarmTime: '2025-03-31 14:00:00',
startPoint: 912.3,
sampleStep: 2.1,
pointCount: 490,
pointInfo: [{ postion: 2, Info: 40 },
{ postion: 4, Info: 44 }]
}
]
// 轨迹点模拟数据
const pointsSource = [
[25.29132248, 99.34718605],
[25.28893705, 99.34728116]
]
// 实时报警点数据
// const alarmPoint = [25.29054437177747, 99.34721946716309];
let alarmMarker = null;
let map = null; // 地图对象
let geojsonLayer = null; // 框选绘制图层对象
let alarmTimer = null; // 报警点更新定时器
let currentDistance = 0; // 当前距离累加器
onMounted(() => {
nextTick(() => {
// 初始化地图对象
initMap();
// 初始化轨迹点及线
initPoints2Line(pointsSource);
// 首页统计图表初始化
initCharts(echartsDataMonth);
});
});
// 组件卸载时清除定时器
onUnmounted(() => {
if (alarmTimer) {
clearInterval(alarmTimer);
alarmTimer = null;
}
});
// 初始化地图
const initMap = () => {
// 创建地图对象
map = L.map("map", {
attributionControl: false,
zoomControl: false,
// maxZoom: 20,
}).setView(config.mapInitParams.center, config.mapInitParams.zoom);
map.on('click', function (e) {
const latlng = e.latlng; // 获取点击位置的经纬度对象
console.log("经度: " + latlng.lng + ", 纬度: " + latlng.lat);
});
//创建底图切换数据源
const baseLayer1 = L.tileLayer(config.baseMaps[0].Url); //OSM街道图
const baseLayer2 = L.tileLayer(config.baseMaps[1].Url); //ArcGIS影像图
const baseLayer4 = L.layerGroup([L.tileLayer(config.baseMaps[3].Url, {
subdomains: ["0", "1", "2", "3", "4", "5", "6", "7"],
}), L.tileLayer(config.baseMaps[4].Url, {
subdomains: ["0", "1", "2", "3", "4", "5", "6", "7"],
})]); //天地图街道图
const baseLayer5 = L.layerGroup([L.tileLayer(config.baseMaps[5].Url, {
subdomains: ["0", "1", "2", "3", "4", "5", "6", "7"],
}), L.tileLayer(config.baseMaps[6].Url, {
subdomains: ["0", "1", "2", "3", "4", "5", "6", "7"],
})]); //天地图影像图
const baseLayer8 = L.tileLayer(config.baseMaps[9].Url, {
subdomains: ["1", "2", "3", "4"],
}); //高德街道图
const baseLayer9 = L.tileLayer(config.baseMaps[10].Url, {
subdomains: ["1", "2", "3", "4"],
}); //高德影像图
// 构建图层标题及图例
const getTitle = (text, borderColor, fillColor, isBorderDashed) => {
return `<i style='display:inline-block;border:${isBorderDashed ? "dashed" : "solid"
} 2px ${borderColor};background:${fillColor};width:20px;height:20px;position:relative;top:4px;'></i>
<span style='padding-left:1px;margin-top: 2px;position: relative;top:1px;'>${text}</span>`;
};
// 构建图片形式的标题及图例
const getImageTitle = (text, imgUrl, size) => {
return `<div style='display:inline-block;width:${size}px;height:${size}px;position:relative;top:4px;'><img src='${imgUrl}' style='height:${size}px;'/></div>
<span style='padding-left:1px;margin-top: 2px;position: relative;top:1px;'>${text}</span>`;
};
map.addLayer(baseLayer2); //地图默认加载的底图
// map.addLayer(labelPointLayer); //地图默认加载的叠加图层
const baseLayers = {
[getImageTitle(`OSM街道图`, `./img/OSMVector.png`, 35)]: baseLayer1,
[getImageTitle(`ArcGIS影像图`, `./img/arcgisImage.png`, 35)]: baseLayer2,
[getImageTitle(`天地图街道图`, `./img/tdtVector.png`, 35)]: baseLayer4,
[getImageTitle(`天地图影像图`, `./img/tdtImage.png`, 35)]: baseLayer5,
[getImageTitle(`高德街道图`, `./img/gaodeVector.png`, 35)]: baseLayer8,
[getImageTitle(`高德街道图`, `./img/gaodeImage.png`, 35)]: baseLayer9,
};
//底图切换控件
// const OHTER_SPOT_COLOR = "#006fff";
// const SPOT_FILL_COLOR = "rgba(0,0,255,0.15)";
// 专题图层
const overlayMaps = {
// [getImageTitle(`充电站`, `./img/labelPointMarker.png`, 20)]:
// labelPointLayer,
// [getImageTitle("热力图", `./img/heatmap.png`, 25)]: heatmapLayer,
};
//底图切换控件
L.control.layers(baseLayers, overlayMaps, { position: 'bottomright' }).addTo(map);
};
// 自定义图标
// const customIcon = L.icon({
// iconUrl: './public/img/labelPointMarker.png', // 图标图片路径
// iconSize: [30, 30], // 图标大小(宽, 高)
// });
const getCustomIcon = (iconUrl, iconSize) => {
return L.icon({
iconUrl: iconUrl,
iconSize: iconSize,
iconAnchor: [iconSize[0] / 2, iconSize[1]]
});
};
// 创建轨迹点及点连线
const initPoints2Line = (points) => {
if (points.length > 0) {
// 创建起点和终点
for (let i = 0; i < points.length; i++) {
let marker = L.marker(
new L.LatLng(
points[i][0],
points[i][1]
),
{
properties: {},
icon: getCustomIcon(`./public/img/${i === 0 ? 'starPoint.png' : 'endPoint.png'}`, [42, 42]) // marker点图标
}
).addTo(map);
}
// 起点终点连线
const polyline = L.polyline(points,
{
// color: '#1f9653',
weight: 6
}).addTo(map);
// 模拟实时报警点更新
const totalDistance = getDistanceBy2Points(pointsSource[0], pointsSource[1]);
currentDistance = 0; // 重置当前距离
// 清除可能存在的旧定时器
if (alarmTimer) {
clearInterval(alarmTimer);
}
refreshAlarmPoint(currentDistance + 10);
// 设置定时器,每5秒执行一次,距离累加5米
alarmTimer = setInterval(() => {
currentDistance += 10; // 每次距离累加5米
// 如果超过总距离,重置为起点
if (currentDistance > totalDistance) {
currentDistance = 0;
}
refreshAlarmPoint(currentDistance);
}, 3000); // 3秒执行一次
}
};
// 刷新绘制实时报警点
const refreshAlarmPoint = (distance) => {
const alarmPoint = getPointAlongLine(pointsSource[0], pointsSource[1], distance);
if (alarmPoint) {
if (alarmMarker) {
alarmMarker.remove();
}
alarmMarker = L.marker(alarmPoint, {
icon: L.divIcon({
className: 'animated-gif-icon',
iconSize: [40, 40] // 设置图标大小以匹配 GIF 的大小
})
}).addTo(map);
// alarmMarker.on('click', function () {
// const popup = L.popup()
// .setLatLng(alarmMarker.getLatLng())
// .setContent('我是一个弹窗')
// .openOn(map);
// });
const properties = tableData[0];
map.closePopup();
openMapPopup(properties);
}
}
// 构造地图弹窗内容
const getPopupContent = (properties) => {
return `预警点所属标桩名称:${properties.siteName}</br></br>预警类型:${properties.alarmType}</br></br>预警级别:${properties.alarmLevel}</br></br>
预警时间:${properties.alarmTime}</br></br>坐标:${properties.lon},${properties.lat}`;
};
// 搜索结果列表点击定位地图弹窗
const openMapPopup = (properties) => {
const elements = getPopupContent(properties);
// const latlng = L.latLng(properties.lat, properties.lon);
const latlng = alarmMarker.getLatLng();
map.openPopup(elements, latlng);
// map.setView(latlng, 15);
}
// 复位
const resetFun = () => {
map.setView(config.mapInitParams.center, config.mapInitParams.zoom);
}
// 首页 月 周 天切换
const handleChange = (value) => {
if (value === 1) {
alarmEventTal.value = {
emergency: 100,
important: 85,
general: 200,
prompt: 96
}
initCharts(echartsDataMonth);
} else if (value === 2) {
alarmEventTal.value = {
emergency: 70,
important: 65,
general: 150,
prompt: 56
}
initCharts(echartsDataWeek);
} else if (value === 3) {
alarmEventTal.value = {
emergency: 40,
important: 45,
general: 100,
prompt: 36
}
initCharts(echartsDataDay);
}
}
// 首页统计图表初始化
const initCharts = (echartsData) => {
// 基于准备好的dom,初始化echarts实例
const myChartDom = document.getElementById('echartsId');
if (myChartDom) {
myChartEvent?.dispose();
}
myChartEvent = echarts.init(myChartDom);
const option = {
title: {
text: '告警事件统计分析',
left: 'center',
// left: '1%',
top: '3%',
textStyle: {
color: '#fff',
fontSize: '16',
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
backgroundColor: 'rgba(0, 0, 0, 0.35)', // 提示框背景颜色
textStyle: {
color: '#fff', // 提示框文本颜色
fontSize: 14, // 文本字体大小
// fontFamily: 'Arial' // 文本字体
},
// borderColor: '#000', // 设置Tooltip边框颜色为黑色
borderWidth: 0, // 设置Tooltip边框宽度为2像素
},
grid: {
left: '3%',
top: '12%',
right: '3%',
bottom: '3%',
containLabel: true
},
xAxis: [{
type: 'category',
data: echartsData.xAxis,
axisLine: {
show: true,
lineStyle: {
color: "rgba(255,255,255,.1)",
width: 1,
type: "solid"
},
},
axisTick: {
show: false,
},
axisLabel: {
interval: 0,
// rotate:50,
show: true,
splitNumber: 15,
textStyle: {
color: "rgba(255,255,255,.6)",
fontSize: '12',
},
},
}],
yAxis: [{
type: 'value',
axisLabel: {
//formatter: '{value} %'
show: true,
textStyle: {
color: "rgba(255,255,255,.6)",
fontSize: '12',
},
},
axisTick: {
show: false,
},
axisLine: {
show: true,
lineStyle: {
color: "rgba(255,255,255,.1 )",
width: 1,
type: "solid"
},
},
splitLine: {
lineStyle: {
color: "rgba(255,255,255,.1)",
}
}
}],
series: [
{
type: 'bar',
data: echartsData.yAxis,
barWidth: '35%', //柱子宽度
itemStyle: {
normal: {
color: '#2f89cf',
opacity: 1,
barBorderRadius: 5,
}
},
tooltip: {
valueFormatter: function (value) {
return value + ' 个';
}
}
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChartEvent.setOption(option);
window.addEventListener("resize", function () {
myChartEvent?.resize();
});
}
// 数据诊断统计图表初始化
const initDataCharts = () => {
// prettier-ignore
// timeData = timeData.map(function (str) {
// return str.replace('2009/', '');
// });
const option = {
// title: {
// text: '温度',
// left: 'center'
// },
tooltip: {
trigger: 'axis',
axisPointer: {
animation: false
},
formatter: function (params) {
// console.log('params:', params);
let result = params[0].name + ' 米' + '<br/>';
for (let i = 0; i < params.length; i++) {
result += params[i].marker + params[i].seriesName + ': ' + params[i].value + ' ℃<br/>';
}
return result;
},
},
// legend: {
// data: ['温度'],
// left: 10
// },
// toolbox: {
// feature: {
// dataZoom: {
// yAxisIndex: 'none'
// },
// restore: {},
// saveAsImage: {}
// }
// },
axisPointer: {
link: [
{
xAxisIndex: 'all'
}
]
},
dataZoom: [
{
show: true,
realtime: true,
textStyle: {
color: '#fff' // 设置字体颜色为红色
},
start: 30,
end: 70,
xAxisIndex: [0, 1]
},
{
type: 'inside',
textStyle: {
color: '#fff' // 设置字体颜色为红色
},
realtime: true,
start: 30,
end: 70,
xAxisIndex: [0, 1]
}
],
grid: [
{
left: 60,
right: 50,
height: '35%'
},
{
left: 60,
right: 50,
top: '55%',
height: '35%'
}
],
xAxis: [
{
type: 'category',
name: '米',
nameTextStyle: {//y轴上方单位的颜色
color: '#fff'
},
boundaryGap: false,
axisLine: { onZero: true },
data: timeXData,
axisLabel: {
// interval: 0,
// rotate:50,
show: true,
// splitNumber: 15,
textStyle: {
color: "rgba(255,255,255,1)",
fontSize: '12',
},
},
}
],
yAxis: [
{
name: '温度(℃)',
type: 'value',
max: 60,
nameTextStyle: {//y轴上方单位的颜色
color: '#fff'
},
axisLabel: {
//formatter: '{value} %'
show: true,
textStyle: {
color: "rgba(255,255,255,1)",
fontSize: '12',
},
}
}
],
series: [
{
name: '温度',
type: 'line',
symbolSize: 8,
// prettier-ignore
data: timeYData
}
]
};
nextTick(() => {
// 基于准备好的dom,初始化echarts实例
const myChartDom = document.getElementById('echartsIdData');
if (myChartDom) {
dataChartEvent?.dispose();
}
dataChartEvent = echarts.init(myChartDom);
// 使用刚指定的配置项和数据显示图表。
dataChartEvent.setOption(option);
window.addEventListener("resize", function () {
dataChartEvent?.resize();
});
})
}
// 告警系统面板
const openAlarmDialog = () => {
// reSetStatus();
isShowDataSystem.value = false;
dataDialogVisible.value = false;
isShowAlarmSystem.value = !isShowAlarmSystem.value;
alarmDialogVisible.value = !alarmDialogVisible.value;
isShowAlarmSystem.value ? isShowLeftMenu.value = false : isShowLeftMenu.value = true;
// isShowLeftMenu.value = false;
}
// 数据诊断面板
const openDataDialog = () => {
// reSetStatus();
isShowAlarmSystem.value = false;
alarmDialogVisible.value = false;
isShowDataSystem.value = !isShowDataSystem.value;
dataDialogVisible.value = !dataDialogVisible.value;
isShowDataSystem.value ? isShowLeftMenu.value = false : isShowLeftMenu.value = true;
// isShowLeftMenu.value = false;
if (dataDialogVisible.value) {
nextTick(() => {
// 数据诊断统计图表初始化
initDataCharts();
})
}
}