- 大屏基础界面:
- 抠出江阴市,其他范围添加遮罩,默认打开其中个别开关
遮罩代码:
// 江阴边界填充图层
const boundarySource = new VectorSource({
features: new GeoJSON().readFeatures(jiangyinArea.jiangyinArea, {
featureProjection: 'EPSG:3857'
})
})
const boundaryLayer = new VectorLayer({
source: boundarySource,
style: createBoundaryStyle,
zIndex: 1 // 确保在底图之上,WMS图层之下
})
// 创建遮罩层
const maskLayer = createMaskLayer()
function createMaskLayer() {
// 世界范围坐标 (EPSG:3857)
const worldExtent = [-20037508.34, -20037508.34, 20037508.34, 20037508.34]
// 创建世界范围多边形
const worldPolygon = new Polygon([[
[worldExtent[0], worldExtent[1]],
[worldExtent[2], worldExtent[1]],
[worldExtent[2], worldExtent[3]],
[worldExtent[0], worldExtent[3]],
[worldExtent[0], worldExtent[1]]
]])
// 解析江阴区域GeoJSON
const jiangyinFeatures = new GeoJSON().readFeatures(jiangyinArea.jiangyinBoundery, {
featureProjection: 'EPSG:3857'
})
// 获取江阴区域坐标(只取第一个多边形)
const jiangyinCoords = jiangyinFeatures[0].getGeometry().getCoordinates()
// 创建遮罩Feature:在世界多边形中"挖空"江阴区域
const maskFeature = new Feature({
geometry: new Polygon([
worldPolygon.getCoordinates()[0], // 外环:世界范围
...jiangyinCoords.map(ring => ring.slice().reverse()) // 内环:江阴区域(坐标需反转)
])
})
// 创建矢量图层
return new VectorLayer({
source: new VectorSource({ features: [maskFeature] }),
style: new Style({
fill: new Fill({
color: 'rgba(6, 18, 50, 0.3)' // 半透明黑色遮罩
}),
stroke: new Stroke({
color: 'transparent' // 隐藏边界线
})
}),
zIndex: 1 // 确保在底图之上
})
}
- 共有三种图层,分别为gis server直接请求加载的瓦片、接口得到经纬度生成点位图层;点聚合图层。将不同类型放入不同图层,之后新加类型只需复制粘贴即可。聚合效果使用leaflet,聚合或散开会有动画效果,点击聚合点位也会有动画效果
核心代码及节流函数:
//点聚合样式
import * as L from 'leaflet'
import 'leaflet.markercluster'
import '@/assets/styles/MarkerCluster.css'
import AnimatedCluster from 'ol-ext/layer/AnimatedCluster' // 图层
import ClusterSource from 'ol/source/Cluster' // 真正的聚类源import * as olEasing from 'ol/easing'
import * as olEasing from 'ol/easing'
//点击聚合点后散开
import { extend as extendExtent, createEmpty } from 'ol/extent';
// 点聚合相关
//点聚合距离
const clusterDistance = ref(100);
//自定义聚合数据(接口获取)
let autoClusterMarkerList = {
'报装':[],
'排水户':[],
'营销':[],
}
//自定义聚合源
let clusterSourceList = {
'报装':null,
'排水户':null,
'营销':null,
}
//自定义聚合源图层
let clusterSourceLayerList = {
'报装':null,
'排水户':null,
'营销':null,
}
//添加点标记到聚合图层
function generateClusPointsToLayer() {
['报装', '排水户', '营销'].forEach(key => {
const source = clusterSourceList[key] // 这就是 VectorSource
if (!source) return
source.clear() // 直接清
const arr = autoClusterMarkerList[key]
if (!arr || !arr.length) return
arr.forEach(item => {
if (!item.latLng || !item.latLng.includes(',')) return
const [lng, lat] = item.latLng.split(',').map(Number)
const feature = new Feature({
geometry: new Point(fromLonLat([lng, lat]))
})
feature.set('data', item)
source.addFeature(feature)
})
})
}
//初始化地图
function initMap() {
// 天地图key轮换 zq
const tdtKeys = globalConfigs.getTdtKeys();
// 计数器用localStorage
let tdtKeyIndex = parseInt(localStorage.getItem('tdtKeyIndex'), 10);
if (isNaN(tdtKeyIndex)) {
tdtKeyIndex = 0;
} else {
tdtKeyIndex = (tdtKeyIndex + 1) % tdtKeys.length;
}
localStorage.setItem('tdtKeyIndex', tdtKeyIndex);
const tdtKey = tdtKeys[tdtKeyIndex];
// 天地图矢量底图服务URL,随机选择天地图二级域名 t0-t7
const tdtSubdomain = Math.floor(Math.random() * 8)
const tdtUrl = `https://t${tdtSubdomain}.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=${tdtKey}`
const tdtLayer = new TileLayer({
source: new XYZ({
url: tdtUrl,
minZoom: MIN_ZOOM,
maxZoom: MAX_ZOOM,
crossOrigin: 'anonymous', // 避免跨域导致瓦片加载异常
}),
cacheSize: 512 // 缓存512个瓦片
})
// 江阴边界填充图层
const boundarySource = new VectorSource({
features: new GeoJSON().readFeatures(jiangyinArea.jiangyinArea, {
featureProjection: 'EPSG:3857'
})
})
const boundaryLayer = new VectorLayer({
source: boundarySource,
style: createBoundaryStyle,
zIndex: 1 // 确保在底图之上,WMS图层之下
})
// 创建遮罩层
const maskLayer = createMaskLayer()
function createMaskLayer() {
// 世界范围坐标 (EPSG:3857)
const worldExtent = [-20037508.34, -20037508.34, 20037508.34, 20037508.34]
// 创建世界范围多边形
const worldPolygon = new Polygon([[
[worldExtent[0], worldExtent[1]],
[worldExtent[2], worldExtent[1]],
[worldExtent[2], worldExtent[3]],
[worldExtent[0], worldExtent[3]],
[worldExtent[0], worldExtent[1]]
]])
// 解析江阴区域GeoJSON
const jiangyinFeatures = new GeoJSON().readFeatures(jiangyinArea.jiangyinBoundery, {
featureProjection: 'EPSG:3857'
})
// 获取江阴区域坐标(只取第一个多边形)
const jiangyinCoords = jiangyinFeatures[0].getGeometry().getCoordinates()
// 创建遮罩Feature:在世界多边形中"挖空"江阴区域
const maskFeature = new Feature({
geometry: new Polygon([
worldPolygon.getCoordinates()[0], // 外环:世界范围
...jiangyinCoords.map(ring => ring.slice().reverse()) // 内环:江阴区域(坐标需反转)
])
})
// 创建矢量图层
return new VectorLayer({
source: new VectorSource({ features: [maskFeature] }),
style: new Style({
fill: new Fill({
color: 'rgba(6, 18, 50, 0.3)' // 半透明黑色遮罩
}),
stroke: new Stroke({
color: 'transparent' // 隐藏边界线
})
}),
zIndex: 1 // 确保在底图之上
})
}
// GeoServer WMS 图层 ---供水管网
let curheight = 300;
const wmsSource = new TileWMS({
url: '/geoserver/work1/wms', // 通过代理转发
params: {
'SERVICE': 'WMS',
'VERSION': '1.1.0',
'REQUEST': 'GetMap',
'LAYERS': 'work1:pipe_network',
'STYLES': 'line_green',
'FORMAT': 'image/png', // 必须用image/png或image/jpeg
// 'FORMAT': 'application/json;type=utfgrid',
'SRS': 'EPSG:3857', // 1.1.1用SRS,1.3.0用CRS
'TILED': false, // 必须为false,否则GeoServer可能返回空图
'TRANSPARENT': true,
// 'CQL_FILTER' 动态设置,初始值为300
'CQL_FILTER': `height > ${curheight}`,
// 'maxFeatures': 5000, //控制显示密度,随机过滤,而不是密度过滤
// 'format_options': 'cluster:true;cluster_distance:60;cluster_max:5', //仅适用于 application/vnd.mapbox-vector-tile 等矢量格式,对 PNG/JPG 栅格格式无效
// 'CQL_FILTER': 'ST_Area(geom) > 10', // 过滤小面积要素,小短线,小面积图形
// 'CQL_FILTER': 'Length(geom) >= 110', // 过滤小短线
// BBOX参数由OpenLayers自动生成并拼接,无需手动设置
},
serverType: 'geoserver',
tileGrid: new TileGrid({
extent: [13356624.394745126, 3722943.2809553347, 13424618.821758036, 3757885.6374882166], // EPSG:3857 全球范围
resolutions: [
611.49622628141, // 级别 8
305.748113140705, // 级别 9
152.8740565703525, // 级别 10
76.43702828517625, // 级别 11
38.21851414258813, // 级别 12
19.109257071294063, // 级别 13
9.554628535647032, // 级别 14
4.777314267823516, // 级别 15
2.388657133911758, // 级别 16
1.194328566955879, // 级别 17
0.5971642834779395, // 级别 18
0.29858214173896974, // 级别 19
0.14929107086948487, // 级别 20
0.0746455354243517, // 级别 21
0.0373227677121758, // 级别 22
0.0186613838560879, // 级别 23
]
}),
crossOrigin: 'anonymous'
// projection: 'EPSG:3857', // OpenLayers 6+不需要此项,view的projection决定
})
// GeoServer WMS 图层 ---污水管网
const wmsSource2 = new TileWMS({
url: '/geoserver/work1/wms', // 通过代理转发
params: {
'SERVICE': 'WMS',
'VERSION': '1.1.0',
'REQUEST': 'GetMap',
'LAYERS': 'work1:pipe_dirty',
'STYLES': 'line_orange',
'FORMAT': 'image/png', // 必须用image/png或image/jpeg
// 'FORMAT': 'application/json;type=utfgrid',
'SRS': 'EPSG:3857', // 1.1.1用SRS,1.3.0用CRS
'TILED': false, // 必须为false,否则GeoServer可能返回空图
'TRANSPARENT': true,
// 'CQL_FILTER' 动态设置,初始值为300
'CQL_FILTER': `height > ${curheight} AND pipe_type = '污水管网'`,
// 'maxFeatures': 5000, //控制显示密度,随机过滤,而不是密度过滤
// 'format_options': 'cluster:true;cluster_distance:60;cluster_max:5', //仅适用于 application/vnd.mapbox-vector-tile 等矢量格式,对 PNG/JPG 栅格格式无效
// 'CQL_FILTER': 'ST_Area(geom) > 10', // 过滤小面积要素,小短线,小面积图形
// 'CQL_FILTER': 'Length(geom) >= 110', // 过滤小短线
// BBOX参数由OpenLayers自动生成并拼接,无需手动设置
},
serverType: 'geoserver',
tileGrid: new TileGrid({
extent: [13356624.394745126, 3722943.2809553347, 13424618.821758036, 3757885.6374882166], // EPSG:3857 全球范围
resolutions: [
611.49622628141, // 级别 8
305.748113140705, // 级别 9
152.8740565703525, // 级别 10
76.43702828517625, // 级别 11
38.21851414258813, // 级别 12
19.109257071294063, // 级别 13
9.554628535647032, // 级别 14
4.777314267823516, // 级别 15
2.388657133911758, // 级别 16
1.194328566955879, // 级别 17
0.5971642834779395, // 级别 18
0.29858214173896974, // 级别 19
0.14929107086948487, // 级别 20
0.0746455354243517, // 级别 21
0.0373227677121758, // 级别 22
0.0186613838560879, // 级别 23
]
}),
crossOrigin: 'anonymous'
// projection: 'EPSG:3857', // OpenLayers 6+不需要此项,view的projection决定
})
// GeoServer WMS 图层 ---污水管网
const wmsSource3 = new TileWMS({
url: '/geoserver/work1/wms', // 通过代理转发
params: {
'SERVICE': 'WMS',
'VERSION': '1.1.0',
'REQUEST': 'GetMap',
'LAYERS': 'work1:pipe_dirty',
'STYLES': 'line_blue',
'FORMAT': 'image/png', // 必须用image/png或image/jpeg
// 'FORMAT': 'application/json;type=utfgrid',
'SRS': 'EPSG:3857', // 1.1.1用SRS,1.3.0用CRS
'TILED': false, // 必须为false,否则GeoServer可能返回空图
'TRANSPARENT': true,
// 'CQL_FILTER' 动态设置,初始值为300
'CQL_FILTER': `height > ${curheight} AND pipe_type = '雨水管网'`,
// 'maxFeatures': 5000, //控制显示密度,随机过滤,而不是密度过滤
// 'format_options': 'cluster:true;cluster_distance:60;cluster_max:5', //仅适用于 application/vnd.mapbox-vector-tile 等矢量格式,对 PNG/JPG 栅格格式无效
// 'CQL_FILTER': 'ST_Area(geom) > 10', // 过滤小面积要素,小短线,小面积图形
// 'CQL_FILTER': 'Length(geom) >= 110', // 过滤小短线
// BBOX参数由OpenLayers自动生成并拼接,无需手动设置
},
serverType: 'geoserver',
tileGrid: new TileGrid({
extent: [13356624.394745126, 3722943.2809553347, 13424618.821758036, 3757885.6374882166], // EPSG:3857 全球范围
resolutions: [
611.49622628141, // 级别 8
305.748113140705, // 级别 9
152.8740565703525, // 级别 10
76.43702828517625, // 级别 11
38.21851414258813, // 级别 12
19.109257071294063, // 级别 13
9.554628535647032, // 级别 14
4.777314267823516, // 级别 15
2.388657133911758, // 级别 16
1.194328566955879, // 级别 17
0.5971642834779395, // 级别 18
0.29858214173896974, // 级别 19
0.14929107086948487, // 级别 20
0.0746455354243517, // 级别 21
0.0373227677121758, // 级别 22
0.0186613838560879, // 级别 23
]
}),
crossOrigin: 'anonymous'
// projection: 'EPSG:3857', // OpenLayers 6+不需要此项,view的projection决定
})
wmsLayer = new TileLayer({
source: wmsSource,
opacity: 1,
cacheSize: 256, // 缓存256个WMS瓦片
visible: false // 初始状态显示
})
wmsLayer.set('layerName', 'layerName01')
wmsLayer2 = new TileLayer({
source: wmsSource2,
opacity: 1,
cacheSize: 256, // 缓存256个WMS瓦片
visible: false // 初始状态显示
})
wmsLayer2.set('layerName', 'layerName02')
wmsLayer3 = new TileLayer({
source: wmsSource3,
opacity: 1,
cacheSize: 256, // 缓存256个WMS瓦片
visible: false // 初始状态显示
})
wmsLayer3.set('layerName', 'layerName03')
//点位
let markerSourceList = {
'雨水井':null,
'污水井':null,
'阀门':null,
'消防栓':null,
'污水泵站':null,
'污水厂范围':null,
'污水厂实体':null,
}
function getMarkerSource(CQL_FILTER,STYLES,LAYERS){
let params = {
'SERVICE': 'WMS',
'VERSION': '1.1.0',
'REQUEST': 'GetMap',
'FORMAT': 'image/png', // 必须用image/png或image/jpeg
// 'FORMAT': 'application/json;type=utfgrid',
'SRS': 'EPSG:3857', // 1.1.1用SRS,1.3.0用CRS
'TILED': false, // 必须为false,否则GeoServer可能返回空图
'TRANSPARENT': true,
// 'CQL_FILTER' 动态设置,初始值为300
// 'maxFeatures': 5000, //控制显示密度,随机过滤,而不是密度过滤
// 'format_options': 'cluster:true;cluster_distance:60;cluster_max:5', //仅适用于 application/vnd.mapbox-vector-tile 等矢量格式,对 PNG/JPG 栅格格式无效
// 'CQL_FILTER': 'ST_Area(geom) > 10', // 过滤小面积要素,小短线,小面积图形
// 'CQL_FILTER': 'Length(geom) >= 110', // 过滤小短线
// BBOX参数由OpenLayers自动生成并拼接,无需手动设置
}
if(CQL_FILTER){
params.CQL_FILTER = CQL_FILTER
}
if(STYLES){
params.STYLES = STYLES
}
if(LAYERS){
params.LAYERS = LAYERS
}
return new TileWMS({
url: '/geoserver/work1/wms', // 通过代理转发
params: params,
serverType: 'geoserver',
tileGrid: new TileGrid({
extent: [13356624.394745126, 3722943.2809553347, 13424618.821758036, 3757885.6374882166], // EPSG:3857 全球范围
resolutions: [
611.49622628141, // 级别 8
305.748113140705, // 级别 9
152.8740565703525, // 级别 10
76.43702828517625, // 级别 11
38.21851414258813, // 级别 12
19.109257071294063, // 级别 13
9.554628535647032, // 级别 14
4.777314267823516, // 级别 15
2.388657133911758, // 级别 16
1.194328566955879, // 级别 17
0.5971642834779395, // 级别 18
0.29858214173896974, // 级别 19
0.14929107086948487, // 级别 20
0.0746455354243517, // 级别 21
0.0373227677121758, // 级别 22
0.0186613838560879, // 级别 23
]
}),
crossOrigin: 'anonymous'
// projection: 'EPSG:3857', // OpenLayers 6+不需要此项,view的projection决定
})
}
markerSourceList['雨水井'] =getMarkerSource('dno=10','point_rain','work1:dirty_point')
markerSourceList['污水井'] =getMarkerSource('dno=13','point_sewage_well','work1:dirty_point')
markerSourceList['阀门'] =getMarkerSource('','point_valve','work1:control')
markerSourceList['消防栓'] =getMarkerSource('','point_fire_hyd','work1:fire_hyd')
markerSourceList['污水泵站'] =getMarkerSource('SUBTYPE=2','point_dirty_pump','work1:ps_pump')
markerSourceList['污水厂范围'] =getMarkerSource('','','work1:wsc_area')
markerSourceList['污水厂实体'] =getMarkerSource('','','work1:wsc')
function getMarkerLayer(source) {
return new TileLayer({
source: source,
opacity: 1,
cacheSize: 256, // 缓存256个WMS瓦片
visible: false // 初始状态显示
})
}
markerList['雨水井'] =getMarkerLayer(markerSourceList['雨水井'])
markerList['污水井'] =getMarkerLayer(markerSourceList['污水井'])
markerList['阀门'] =getMarkerLayer(markerSourceList['阀门'])
markerList['消防栓'] =getMarkerLayer(markerSourceList['消防栓'])
markerList['污水泵站'] =getMarkerLayer(markerSourceList['污水泵站'])
markerList['污水厂范围'] =getMarkerLayer(markerSourceList['污水厂范围'])
markerList['污水厂实体'] =getMarkerLayer(markerSourceList['污水厂实体'])
markerList['雨水井'].set('layerName', '雨水井')
markerList['污水井'].set('layerName', '污水井')
markerList['阀门'].set('layerName', '阀门')
markerList['消防栓'].set('layerName', '消防栓')
markerList['污水泵站'].set('layerName', '污水泵站')
markerList['污水厂范围'].set('layerName', '污水厂范围')
markerList['污水厂实体'].set('layerName', '污水厂实体')
autoMarkerLayerList['电导率'] = new VectorLayer({
source: new VectorSource(),
})
autoMarkerLayerList['水厂'] = new VectorLayer({
source: new VectorSource(),
})
autoMarkerLayerList['水源地'] = new VectorLayer({
source: new VectorSource(),
})
autoMarkerLayerList['加压站'] = new VectorLayer({
source: new VectorSource(),
})
autoMarkerLayerList['二供工单'] = new VectorLayer({
source: new VectorSource(),
})
autoMarkerLayerList['高品质水'] = new VectorLayer({
source: new VectorSource(),
})
autoMarkerLayerList['直饮水机'] = new VectorLayer({
source: new VectorSource(),
})
autoMarkerLayerList['消防压力'] = new VectorLayer({
source: new VectorSource(),
})
autoMarkerLayerList['工程项目'] = new VectorLayer({
source: new VectorSource(),
})
//点聚合图层
clusterSourceList['报装'] = new VectorSource();
clusterSourceList['排水户'] = new VectorSource();
clusterSourceList['营销'] = new VectorSource();
// 创建聚合数据源
const animOptions = {
animationDuration: 600,
easing: olEasing.easeOut,
spread: 36
}
// 创建聚合图层样式
const clusterStyle = (feature) => {
const features = feature.get('features') // AnimatedCluster 里一定存在
const size = features.length
const item = features[0].get('data')
const markerImg = item.markerImg
// 单点:直接返回图标,但保留爆炸能力
if (size === 1) {
return new Style({
image: new Icon({
anchor: [0.5, 0.5],
src: markerImg,
scale: 0.4,
zIndex: 9999
})
})
}
// 聚合圈
const radius = Math.min(20 + Math.sqrt(size) * 5, 40)
const color =
size > 50 ? '#d32f2f' : size > 20 ? '#f57c00' : size > 5 ? '#1976d2' : '#388e3c'
return new Style({
image: new Circle({
radius,
fill: new Fill({ color: color + '80' }),
stroke: new Stroke({ color: '#fff', width: 3 })
}),
text: new Text({
text: size.toString(),
fill: new Fill({ color: '#fff' }),
font: 'bold 16px sans-serif'
})
})
}
/* 3. 直接创建 ol-ext 的 AnimatedCluster 图层(它内部会自己包 ClusterSource) */
const clusterLayerList = {
报装: new AnimatedCluster({
name: '报装',
source: new ClusterSource({
source: clusterSourceList['报装'],
distance: clusterDistance.value
}),
...animOptions,
style: clusterStyle
}),
排水户: new AnimatedCluster({
name: '排水户',
source: new ClusterSource({
source: clusterSourceList['排水户'],
distance: clusterDistance.value
}),
...animOptions,
style: clusterStyle
}),
营销: new AnimatedCluster({
name: '营销',
source: new ClusterSource({
source: clusterSourceList['营销'],
distance: clusterDistance.value
}),
...animOptions,
style: clusterStyle
})
}
//坐标系转换
const extent4326 = [119.75487990670948, 31.606645090235276, 120.86752346270671, 32.08937971257144];
const extent3857 = transformExtent(extent4326, 'EPSG:4326', 'EPSG:3857');
//创建地图
map = new Map({
target: 'ol-map',
layers: [
tdtLayer,
boundaryLayer, // 添加边界图层
maskLayer,//江阴以外的遮罩
wmsLayer,
wmsLayer2,
wmsLayer3,
markerList['雨水井'],
markerList['污水井'],
markerList['阀门'],
markerList['消防栓'],
markerList['污水泵站'],
markerList['污水厂范围'],
markerList['污水厂实体'],
autoMarkerLayerList['电导率'],
autoMarkerLayerList['水厂'],
autoMarkerLayerList['水源地'],
autoMarkerLayerList['加压站'],
autoMarkerLayerList['二供工单'],
autoMarkerLayerList['高品质水'],
autoMarkerLayerList['直饮水机'],
autoMarkerLayerList['消防压力'],
autoMarkerLayerList['工程项目'],
clusterLayerList['报装'],
clusterLayerList['排水户'],
clusterLayerList['营销']
],
view: new View({
center: fromLonLat(lnglatCenter), // fromLonLat 默认转换为 EPSG:3857
zoom: MIN_ZOOM,
minZoom: MIN_ZOOM,
maxZoom: MAX_ZOOM,
projection: 'EPSG:3857',
// 添加以下属性限制拖动范围
extent: extent3857,
constrainOnlyCenter: true, // 只限制中心点,不限制整个视图
constrainResolution: false, // 可选:限制分辨率不能超出范围
smoothExtentConstraint: true // 平滑约束
}),
interactions: defaultInteractions({
// 启用以鼠标位置为中心的缩放
mouseWheelZoom: {
duration: 250,
constrainResolution: false, // 允许中间缩放级别
onFocusOnly: false // 确保不需要聚焦也能缩放
}
}),
controls: defaultControls().extend([new FullScreen(), new ScaleLine()]),
})
// 节流处理的WMS参数更新函数
const updateWmsParams = throttle((zoom) => {
try {
let curheight = 300;
if (zoom >= 12 && zoom < 13) curheight = 250;
else if (zoom >= 13 && zoom < 14) curheight = 200;
else if (zoom >= 14 && zoom < 15) curheight = 150;
else if (zoom >= 15 && zoom < 16) curheight = 100;
else if (zoom >= 16 && zoom < 17) curheight = 50;
else if (zoom >= 17 && zoom <= 18) curheight = 0;
else if (zoom < 12) curheight = 300;
// 确保WMS源存在且有效
if (wmsSource && typeof wmsSource.updateParams === 'function') {
wmsSource.updateParams({
'CQL_FILTER': `height > ${curheight}`
});
}
if (wmsSource2 && typeof wmsSource2.updateParams === 'function') {
wmsSource2.updateParams({
'CQL_FILTER': `height > ${curheight} AND pipe_type = '污水管网'`
});
}
if (wmsSource3 && typeof wmsSource3.updateParams === 'function') {
wmsSource3.updateParams({
'CQL_FILTER': `height > ${curheight} AND pipe_type = '雨水管网'`
});
}
updateAutoMarkerLayerList();
} catch (error) {
console.warn('WMS参数更新失败:', error);
}
}, 50); // 50ms节流,确保及时更新
// 合并的事件监听器:处理缩放限制和WMS参数更新,更新管线详情弹框位置
map.getView().on('change:resolution', function () {
try {
const view = map.getView()
const zoom = view.getZoom()
// 节流更新WMS参数
updateWmsParams(zoom)
if (popupVisible.value && coordinate) {
updatePopupPosition(coordinate);
}
} catch (error) {
console.error('地图缩放事件处理失败:', error);
}
})
// 地图图层实例点击事件
const handleMapClick = throttle(async function (evt) {
/* 0. 清理上一次弹窗状态 */
currentFeature = null
popupTypes.value = []
isProjectPopup.value = false
coordinate = evt.coordinate
/* 1. 优先处理:是否点到了聚合簇 */
let hitCluster = false
map.forEachFeatureAtPixel(evt.pixel, (f, l) => {
const features = f.get('features')
if (features && features.length > 1 && ['报装', '排水户', '营销'].includes(l?.get('name'))) {
hitCluster = true
f.set('select', true)
const extent = features.reduce(
(acc, ft) => {
extendExtent(acc, ft.getGeometry().getExtent());
return acc;
},
createEmpty() // ← 初始值是一个“空范围”
);
/* 下一帧再执行缩放,避免阻塞当前事件循环 */
requestAnimationFrame(() =>
map.getView().fit(extent, { padding: [60, 60, 60, 60], maxZoom: 17 })
)
}
})
/* 只要用于打散,就直接 return,不往下走任何弹窗逻辑 */
if (hitCluster) return
/* 2. 下方保持你原来的单点 / WMS 弹窗逻辑,无任何改动 */
if (!wmsLayer.getVisible() && !wmsLayer2.getVisible() && !wmsLayer3.getVisible() &&
!activeButtons.value.includes('阀门') &&
!activeButtons.value.includes('消防栓') &&
!activeButtons.value.includes('二供工单') &&
!activeButtons.value.includes('高品质水') &&
!activeButtons.value.includes('直饮水机') &&
!activeButtons.value.includes('报装') &&
!activeButtons.value.includes('营销') &&
!activeButtons.value.includes('消防压力') &&
!activeButtons.value.includes('工程项目') &&
!activeButtons.value.includes('加压站') &&
!activeButtons.value.includes('水厂') &&
!activeButtons.value.includes('水源地') &&
!activeButtons.value.includes('雨水井') &&
!activeButtons.value.includes('污水井') &&
!activeButtons.value.includes('电导率') &&
!activeButtons.value.includes('排水户') &&
!activeButtons.value.includes('污水泵站') &&
!activeButtons.value.includes('污水厂')) {
return
}
const viewResolution = map.getView().getResolution()
coordinate = evt.coordinate
if (activeButtons.value.includes('加压站')) {
const destfeatureInfo = map.forEachFeatureAtPixel(evt.pixel, (feature, layer) => {
if (layer === autoMarkerLayerList['加压站']) return { feature, layer }
})
if (destfeatureInfo) {
currentFeature = destfeatureInfo.feature
const data = currentFeature.get('data')
popupTypes.value = ['加压站']
markerInfo.value = `
<div class="info-card">
<div class="info-center">
<span class="info-address-value">${data.waterworksName}</span>
</div>
</div>`
updatePopupPosition(coordinate)
popupVisible.value = true
return
}
}
if (activeButtons.value.includes('水厂')) {
const destfeatureInfo = map.forEachFeatureAtPixel(evt.pixel, (feature, layer) => {
if (layer === autoMarkerLayerList['水厂']) return { feature, layer }
})
if (destfeatureInfo) {
currentFeature = destfeatureInfo.feature
const data = currentFeature.get('data')
popupTypes.value = ['水厂']
markerInfo.value = `
<div class="info-card">
<div class="info-center">
<span class="info-address-value">${data.waterworksName}</span>
</div>
</div>`
updatePopupPosition(coordinate)
popupVisible.value = true
return
}
}
if (activeButtons.value.includes('水源地')) {
const destfeatureInfo = map.forEachFeatureAtPixel(evt.pixel, (feature, layer) => {
if (layer === autoMarkerLayerList['水源地']) return { feature, layer }
})
if (destfeatureInfo) {
currentFeature = destfeatureInfo.feature
const data = currentFeature.get('data')
popupTypes.value = ['水源地']
markerInfo.value = `
<div class="info-card">
<div class="info-center">
<span class="info-address-value">${data.waterworksName}</span>
</div>
</div>`
updatePopupPosition(coordinate)
popupVisible.value = true
return
}
}
if (activeButtons.value.includes('二供工单')) {
const destfeatureInfo = map.forEachFeatureAtPixel(evt.pixel, (feature, layer) => {
if (layer === autoMarkerLayerList['二供工单']) return { feature, layer }
})
if (destfeatureInfo) {
currentFeature = destfeatureInfo.feature
const data = currentFeature.get('data')
popupTypes.value = ['二供工单']
markerInfo.value = `
<div class="info-card">
<div class="info-center"><span class="info-center-name">状态:</span><span class="info-address-value">${data.status}</span></div>
<div class="info-center"><span class="info-center-name">泵房:</span><span class="info-address-value">${data.executor}</span></div>
<div class="info-address"><span class="info-center-name">类型:</span><span class="info-address-value">${data.processType}</span></div>
</div>`
updatePopupPosition(coordinate)
popupVisible.value = true
return
}
}
if (activeButtons.value.includes('高品质水')) {
const destfeatureInfo = map.forEachFeatureAtPixel(evt.pixel, (feature, layer) => {
if (layer === autoMarkerLayerList['高品质水']) return { feature, layer }
})
if (destfeatureInfo) {
currentFeature = destfeatureInfo.feature
const data = currentFeature.get('data')
popupTypes.value = ['高品质水']
markerInfo.value = `
<div class="info-card">
<div class="info-center"><span class="info-center-name">设备名称:</span><span class="info-address-value">${data.equipmentName}</span></div>
<div class="info-center"><span class="info-center-name">状态:</span><span class="info-address-value">${data.equipmentStatus_dict}</span></div>
<div class="info-address"><span class="info-center-name">设备编码:</span><span class="info-address-value">${data.equipmentCode}</span></div>
<div class="info-address"><span class="info-center-name">设备地址:</span><span class="info-address-value">${data.equipmentAddress}</span></div>
</div>`
updatePopupPosition(coordinate)
popupVisible.value = true
return
}
}
if (activeButtons.value.includes('直饮水机')) {
const destfeatureInfo = map.forEachFeatureAtPixel(evt.pixel, (feature, layer) => {
if (layer === autoMarkerLayerList['直饮水机']) return { feature, layer }
})
if (destfeatureInfo) {
currentFeature = destfeatureInfo.feature
const data = currentFeature.get('data')
popupTypes.value = ['直饮水机']
markerInfo.value = `
<div class="info-card">
<div class="info-center"><span class="info-center-name">设备名称:</span><span class="info-address-value">${data.equipmentName}</span></div>
<div class="info-center"><span class="info-center-name">状态:</span><span class="info-address-value">${data.equipmentStatus_dict}</span></div>
<div class="info-address"><span class="info-center-name">设备编码:</span><span class="info-address-value">${data.equipmentCode}</span></div>
<div class="info-address"><span class="info-center-name">设备地址:</span><span class="info-address-value">${data.equipmentAddress}</span></div>
</div>`
updatePopupPosition(coordinate)
popupVisible.value = true
return
}
}
if (activeButtons.value.includes('报装')) {
const destFeatureInfo = map.forEachFeatureAtPixel(evt.pixel, (feature, layer) => {
if (layer === clusterLayerList['报装']) return { feature, layer }
})
if (destFeatureInfo) {
currentFeature = destFeatureInfo.feature
const realFeatures = currentFeature.get('features')
const data = realFeatures[0].get('data')
popupTypes.value = ['报装']
markerInfo.value = `
<div class="info-card">
<div class="info-center"><span class="info-center-name">项目编号:</span><span class="info-address-value">${data.projno}</span></div>
<div class="info-center"><span class="info-center-name">项目名称:</span><span class="info-address-value">${data.projname}</span></div>
<div class="info-address"><span class="info-center-name">设备地址:</span><span class="info-address-value">${data.projaddress}</span></div>
</div>`
updatePopupPosition(coordinate)
popupVisible.value = true
return
}
}
if (activeButtons.value.includes('营销')) {
const destFeatureInfo = map.forEachFeatureAtPixel(evt.pixel, (feature, layer) => {
if (layer === clusterLayerList['营销']) return { feature, layer }
})
if (destFeatureInfo) {
currentFeature = destFeatureInfo.feature
const realFeatures = currentFeature.get('features')
const data = realFeatures[0].get('data')
popupTypes.value = ['营销']
markerInfo.value = `
<div class="info-card">
<div class="info-center"><span class="info-center-name">主键:</span><span class="info-address-value">${data.id}</span></div>
<div class="info-center"><span class="info-center-name">反映地址:</span><span class="info-address-value">${data.reflectaddress}</span></div>
<div class="info-address"><span class="info-center-name">反映详情:</span><span class="info-address-value">${data.reflectdetail}</span></div>
<div class="info-center"><span class="info-center-name">反映用户:</span><span class="info-address-value">${data.reflectuser}</span></div>
<div class="info-address"><span class="info-center-name">咨询主题:</span><span class="info-address-value">${data.serverLarge}</span></div>
<div class="info-center"><span class="info-center-name">咨询类型:</span><span class="info-address-value">${data.serverSmall}</span></div>
<div class="info-address"><span class="info-center-name">反映中心:</span><span class="info-address-value">${data.treatinst}</span></div>
<div class="info-address"><span class="info-center-name">条目备注:</span><span class="info-address-value">${data.entryremark}</span></div>
</div>`
updatePopupPosition(coordinate)
popupVisible.value = true
return
}
}
if (activeButtons.value.includes('排水户')) {
const destFeatureInfo = map.forEachFeatureAtPixel(evt.pixel, (feature, layer) => {
if (layer === clusterLayerList['排水户']) return { feature, layer }
})
if (destFeatureInfo) {
currentFeature = destFeatureInfo.feature
const realFeatures = currentFeature.get('features')
const data = realFeatures[0].get('data')
popupTypes.value = ['排水户']
markerInfo.value = `
<div class="info-card">
<div class="info-center"><span class="info-center-name">排水户名称:</span><span class="info-address-value">${data.name}</span></div>
<div class="info-center"><span class="info-center-name">所属区域:</span><span class="info-address-value">${data.geom}</span></div>
<div class="info-address"><span class="info-center-name">详细地址:</span><span class="info-address-value">${data.address}</span></div>
<div class="info-address"><span class="info-center-name">排水性质:</span><span class="info-address-value">${data.nature}</span></div>
<div class="info-address"><span class="info-center-name">是否重点排水户:</span><span class="info-address-value">${data.isSewUser}</span></div>
<div class="info-address"><span class="info-center-name">行业类型:</span><span class="info-address-value">${data.type}</span></div>
<div class="info-address"><span class="info-center-name">证书:</span><span class="info-address-value">${data.issue}</span></div>
<div class="info-address"><span class="info-center-name">证书总数:</span><span class="info-address-value">${data.issueTotal}</span></div>
<div class="info-center"><span class="info-center-name">工业废水排放量:</span><span class="info-address-value">${data.industryAmount}</span></div>
<div class="info-center"><span class="info-center-name">污水排放总量:</span><span class="info-address-value">${data.dirtyAmount}</span></div>
<div class="info-address"><span class="info-center-name">消防用水量:</span><span class="info-address-value">${data.xiaofangAmount}</span></div>
<div class="info-address"><span class="info-center-name">生产用水量:</span><span class="info-address-value">${data.produceAmount}</span></div>
<div class="info-address"><span class="info-center-name">生活污水排放量:</span><span class="info-address-value">${data.lifeDirtyAmount}</span></div>
<div class="info-address"><span class="info-center-name">生活用水量:</span><span class="info-address-value">${data.lifeAmount}</span></div>
<div class="info-address"><span class="info-center-name">申请表用水量:</span><span class="info-address-value">${data.applyAmount}</span></div>
<div class="info-address"><span class="info-center-name">备注:</span><span class="info-address-value">${data.remark}</span></div>
</div>`
updatePopupPosition(coordinate)
popupVisible.value = true
return
}
}
if (activeButtons.value.includes('消防压力')) {
const destfeatureInfo = map.forEachFeatureAtPixel(evt.pixel, (feature, layer) => {
if (layer === autoMarkerLayerList['消防压力']) return { feature, layer }
})
if (destfeatureInfo) {
currentFeature = destfeatureInfo.feature
const data = currentFeature.get('data')
if (xiaofangDialog.value) xiaofangDialog.value.show(data)
return
}
}
if (activeButtons.value.includes('工程项目')) {
const destfeatureInfo = map.forEachFeatureAtPixel(evt.pixel, (feature, layer) => {
if (layer === autoMarkerLayerList['工程项目']) return { feature, layer }
})
if (destfeatureInfo) {
currentFeature = destfeatureInfo.feature
const data = currentFeature.get('data')
popupTypes.value = ['工程项目']
popupComponent.value = markRaw(ProjectPopup)
popupProps.value = { data }
updatePopupPosition(coordinate)
popupVisible.value = true
return
}
}
if (activeButtons.value.includes('电导率')) {
const destfeatureInfo = map.forEachFeatureAtPixel(evt.pixel, (feature, layer) => {
if (layer === autoMarkerLayerList['电导率']) return { feature, layer }
})
if (destfeatureInfo) {
currentFeature = destfeatureInfo.feature
const data = currentFeature.get('data')
if (conductivityDialog.value) conductivityDialog.value.show(data)
return
}
}
/* -------- 以下 WMS GetFeatureInfo 部分也完全保持原样 -------- */
function getUrl(wmsSourceType, QUERY_LAYERS) {
return wmsSourceType.getFeatureInfoUrl(
coordinate,
viewResolution,
'EPSG:3857',
{
REQUEST: 'GetFeatureInfo',
INFO_FORMAT: 'application/json',
QUERY_LAYERS,
FEATURE_COUNT: 1
}
)
}
const urls = []
const markerList = []
if (wmsLayer.getVisible()) {
urls.push({ url: getUrl(wmsSource, 'work1:pipe_network'), type: 'type1' })
}
if (wmsLayer2.getVisible()) {
urls.push({ url: getUrl(wmsSource2, 'work1:pipe_dirty'), type: 'type2' })
}
if (wmsLayer3.getVisible()) {
urls.push({ url: getUrl(wmsSource3, 'work1:pipe_dirty'), type: 'type3' })
}
if (activeButtons.value.includes('阀门')) {
markerList.push({ url: getUrl(markerSourceList['阀门'], 'work1:control'), type: '阀门' })
}
if (activeButtons.value.includes('消防栓')) {
markerList.push({ url: getUrl(markerSourceList['消防栓'], 'work1:fire_hyd'), type: '消防栓' })
}
if (activeButtons.value.includes('雨水井')) {
markerList.push({ url: getUrl(markerSourceList['雨水井'], 'work1:dirty_point'), type: '雨水井' })
}
if (activeButtons.value.includes('污水井')) {
markerList.push({ url: getUrl(markerSourceList['污水井'], 'work1:dirty_point'), type: '污水井' })
}
if (activeButtons.value.includes('污水泵站')) {
markerList.push({ url: getUrl(markerSourceList['污水泵站'], 'work1:ps_pump'), type: '污水泵站' })
}
if (activeButtons.value.includes('污水厂')) {
markerList.push({ url: getUrl(markerSourceList['污水厂范围'], 'work1:wsc_area'), type: '污水厂范围' })
markerList.push({ url: getUrl(markerSourceList['污水厂实体'], 'work1:wsc'), type: '污水厂实体' })
}
handlerUrl(urls, markerList)
}, 50)
const handlerUrl = async (urls,markerList) => {
//字典
const getLabel = (value) => {
const item = subtype.value.find(opt => opt.value == value);
return item ? item.label : value; // 找不到时显示原值
};
try {
// 遍历所有管线
for (let i=0;i<urls.length;i++) {
if (!urls[i].url) continue; // 跳过无效URL
const response = await fetch(urls[i].url);
const data = await response.json();
// 检查是否有要素信息
if (data && data.features && data.features.length > 0) {
const feature = data.features[0];
let height
let material
let geo_length
let assetstrin
if(urls[i].type == 'type1'){
height = feature.properties?.height ?? '--'
material = feature.properties?.material ?? '--'
geo_length = feature.properties?.geo_length ?? '--'
assetstrin = feature.properties?.assetstrin ?? '--'
}else if(urls[i].type == 'type2'){
height = feature.properties?.height ?? '--'
material = feature.properties['材质'] || '--'
geo_length = feature.properties['管长'] || '--'
assetstrin = feature.properties['所在道'] || '--'
}else if(urls[i].type == 'type3'){
height = feature.properties?.height ?? '--'
material = feature.properties['材质'] || '--'
geo_length = feature.properties['管长'] || '--'
assetstrin = feature.properties['所在道'] || '--'
}
// 显示弹窗(使用对应图层的属性)
markerInfo.value = `
<div class="info-card">
<!-- 管径 -->
<div class="info-center">
<span class="info-center-name">管径:</span>
<span class="info-center-value">
${height}<span style="font-size: 12px; color: #b8d4ff; margin-left: 4px;">mm</span>
</span>
</div>
<!-- 材质 -->
<div class="info-center">
<span class="info-center-name">材质:</span>
<span class="info-center-value">${material}</span>
</div>
<!-- 长度 -->
<div class="info-center">
<span class="info-center-name">长度:</span>
<span class="info-center-value">
${geo_length}<span style="font-size: 12px; color: #b8d4ff; margin-left: 4px;">m</span>
</span>
</div>
<!-- 所在地 -->
<div class="info-address">
<span class="info-center-name">所在地:</span>
<span class="info-address-value">${assetstrin}</span>
</div>
</div>
`;
updatePopupPosition(coordinate); // 更新弹窗位置
popupVisible.value = true;
return
}
}
//遍历标记
for (const marker of markerList) {
if (!marker.url) continue; // 跳过无效URL
const response = await fetch(marker.url);
const data = await response.json();
// 检查是否有要素信息
if (data && data.features && data.features.length > 0) {
const feature = data.features[0];
if(marker.type === '阀门'){
markerInfo.value = `
<div class="info-card">
<!-- 所在地 -->
<div class="info-address">
<span class="info-center-name">所在地:</span>
<span class="info-address-value">${feature.properties?.ADDRESS ?? '--'}</span>
</div>
<!-- 阀门类型 -->
<div class="info-center">
<span class="info-center-name">阀门类型:</span>
<span class="info-center-value">
${getLabel(feature.properties.SUBTYPE)}
</span>
</div>
<!-- 符号角度 -->
<div class="info-angle">
<span class="info-center-name">符号角度:</span>
<span class="angle-value">
${feature.properties?.ANGLE ?? '--'}
</span>
</div>
</div>
`;
}
if(marker.type === '消防栓'){
markerInfo.value = `
<div class="info-card">
<div class="info-address">
<span class="info-center-name">所在地:</span>
<span class="info-address-value">${feature.properties?.ADDRESS ?? '--'}</span>
</div>
<div class="info-center">
<span class="info-center-name">工程名称:</span>
<span class="info-center-value">
${getLabel(feature.properties.PROJECTNAM)}
</span>
</div>
<div class="info-center">
<span class="info-center-name">消防栓类型:</span>
<span class="info-center-value">
${xiaofangshuanDict(feature.properties.SUBTYPE)}
</span>
</div>
<!-- 符号角度 -->
<div class="info-angle">
<span class="info-center-name">符号角度:</span>
<span class="angle-value">
${feature.properties?.ANGLE ?? '--'}
</span>
</div>
</div>
`;
}
if(marker.type === '雨水井' || marker.type === '污水井'){
markerInfo.value = `
<div class="info-card">
<div class="info-address">
<span class="info-center-name">所在道:</span>
<span class="info-address-value">${feature.properties['所在道'] || ''}</span>
</div>
<div class="info-center">
<span class="info-center-name">权属单:</span>
<span class="info-center-value">
${feature.properties['权属单'] || ''}
</span>
</div>
<div class="info-center">
<span class="info-center-name">测绘单:</span>
<span class="info-center-value">
${feature.properties['测绘单'] || ''}
</span>
</div>
<!-- 符号角度 -->
<div class="info-angle">
<span class="info-center-name">符号角度:</span>
<span class="angle-value">
${feature.properties?.angle ?? '--'}
</span>
</div>
</div>
`;
}
if(marker.type === '污水泵站'){
markerInfo.value = `
<div class="info-card">
<div class="info-address">
<span class="info-center-name">泵站名称:</span>
<span class="info-address-value">${feature.properties['NAME'] || ''}</span>
</div>
<div class="info-address">
<span class="info-center-name">探测单位:</span>
<span class="info-address-value">${feature.properties['REPORTDEPT'] || ''}</span>
</div>
<div class="info-angle">
<span class="info-center-name">符号角度:</span>
<span class="angle-value">
${feature.properties?.ANGLE ?? '--'}
</span>
</div>
</div>
`;
}
if(marker.type === '污水厂范围'){
markerInfo.value = `
<div class="info-card">
<div class="info-address">
<span class="info-center-name">污水厂名称:</span>
<span class="info-address-value">${feature.properties['NAME'] || ''}</span>
</div>
</div>
`;
}
updatePopupPosition(coordinate); // 更新弹窗位置
popupVisible.value = true;
return
}
}
// 所有图层都无结果时关闭弹窗
popupVisible.value = false;
} catch (error) {
popupVisible.value = false;
}
}
//绑定事件
map.un('singleclick', handleMapClick);
map.on('singleclick', handleMapClick);
// 点击地图输出经纬度坐标
map.on('singleclick', function (evt) {
// evt.coordinate 是投影坐标(EPSG:3857),需要转换为经纬度
const lonLat = toLonLat(evt.coordinate);
console.log('点击位置经纬度:', lonLat);
});
// 在点击地图其他地方关闭弹窗时也清除高亮
map.on('pointerdown', function (evt) {
// 如果不是单击(即不是singleclick事件),关闭弹窗并清除高亮
if (!evt.originalEvent || evt.originalEvent.type !== 'click') {
popupVisible.value = false;
}
});
}
- 自定义组件,可嵌入监控
// 装载组件
popupComponent.value = markRaw(ProjectPopup)
子组件代码:
<template>
<div class="info-card" style="max-width:400px">
<div class="info-center">
<span class="info-center-name">项目名称:</span>
<span class="info-address-value">{{ data.projectName }}</span>
</div>
<div class="info-center">
<span class="info-center-name">项目编号:</span>
<span class="info-address-value">{{ data.projectCode }}</span>
</div>
<div class="info-address">
<span class="info-center-name">项目类型:</span>
<span class="info-address-value">{{ data.projectType }}</span>
</div>
<div class="info-address">
<span class="info-center-name">工单类型:</span>
<span class="info-address-value">{{ data.flowId }}</span>
</div>
<div class="info-address">
<span class="info-center-name">项目阶段:</span>
<span class="info-address-value">{{ projectPeriodLabel }}</span>
</div>
<div class="info-address">
<span class="info-center-name">项目负责人:</span>
<span class="info-address-value">{{ data.managerUser }}</span>
</div>
<div class="info-address">
<span class="info-center-name">开始时间:</span>
<span class="info-address-value">{{ data.planStartTime }}</span>
</div>
<div class="info-address">
<span class="info-center-name">结束时间:</span>
<span class="info-address-value">{{ data.planEndTime }}</span>
</div>
<div class="info-address">
<span class="info-center-name">联系电话:</span>
<span class="info-address-value">{{ data.managerPhone }}</span>
</div>
<div class="info-address">
<span class="info-center-name">经纬度:</span>
<span class="info-address-value">{{ data.latLng }}</span>
</div>
<!-- 视频 -->
<div class="video-overlay" @click="showVideo"></div>
<iframe
v-if="videoUrl"
:src="videoUrl"
style="width:320px;height:200px;overflow:hidden;border:0;margin-top:8px"
loading="lazy"
allowfullscreen
@load="onIframeLoad"
/>
<video-dialog ref="videoDialog" :title-size="'font-size:24px'"></video-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { getMonitorUrl } from '@/api/screen/map'
import { globalConfigs } from '@/utils/globalConfigs'
import VideoDialog from "./VideoDialog.vue";
// 字典
const { proxy } = getCurrentInstance();
const { pm_project_period } = proxy.useDict('pm_project_period')
const props = defineProps({
data: { type: Object, required: true }
})
const videoDialog = ref()
const msg = ref('')
let iframeLoaded = false
//加载完成
function onIframeLoad(){
iframeLoaded = true
}
//弹层
function showVideo() {
if(!iframeLoaded) return
setTimeout(()=>{
let item = {
msg:msg.value,
monitorName:props.data.projectName
}
if(videoDialog.value) videoDialog.value.show(item)
},100)
}
const videoUrl = ref('')
const ipUrl = globalConfigs.monitorUrl()
const monitorIpUrl = globalConfigs.monitorIpUrl()
const projectPeriodLabel = computed(() => {
const val = pm_project_period.value.find(
item => item.value === props.data.projectPeriod
)
return val?.label ?? props.data.projectPeriod
})
onMounted(async () => {
try {
msg.value = ''
const res = await getMonitorUrl('d8051f4f9f72439f86cf04df15ed998d')
msg.value = res.msg
videoUrl.value = `${ipUrl}?url=${monitorIpUrl}${msg.value}`
} catch (e) {
console.error('获取监控地址失败', e)
}
})
</script>
<style scoped>
.info-card::before {
content: '';
position: absolute;
top: 7px; /* 距离卡片顶部 14px,可微调 */
left: -7px; /* 向左伸出 7px,刚好盖住边框形成无缝三角 */
width: 0;
height: 0;
border-style: solid;
border-width: 6px 7px 6px 0; /* 上 右 下 左:形成一个朝左的三角形 */
border-color: transparent rgba(20, 26, 35, 0.95) transparent transparent;
/* 三角形颜色与卡片背景保持一致 */
}
.white-card::before {
content: '';
position: absolute;
top: 6px; /* 距离卡片顶部 14px,可微调 */
left: -12px; /* 向左伸出 7px,刚好盖住边框形成无缝三角 */
width: 0;
height: 0;
border-style: solid;
border-width: 12px 14px 12px 0; /* 上 右 下 左:形成一个朝左的三角形 */
border-color: transparent rgba(255, 255, 255, 1) transparent transparent;
/* 三角形颜色与卡片背景保持一致 */
}
.info-card {
background: rgba(20, 26, 35, 0.95);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
padding: 14px 16px;
min-width: 220px;
max-width: 280px;
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 13px;
line-height: 1.4;
color: #fff;
backdrop-filter: blur(4px);
position: relative; /* 让箭头相对它定位 */
}
.white-card{
background: white;
border-radius: 10px 10px 10px 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
padding: 18px;
min-width: 280px;
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
border: 1px solid #eee;
position: relative; /* 让箭头相对它定位 */
}
.info-address{
display: flex;
margin-bottom: 8px;
align-items: flex-start;
}
.info-address-value{
flex:1;
word-break:break-all;
}
.info-center{
display: flex;
margin-bottom: 8px;
align-items: center;
}
.info-center-name{
flex:0 0 80px;
color:#8ab4f8;
font-weight:600;
}
.info-center-value{
flex:1;
color:#1a73e8;
font-weight:500;
padding:4px 8px;
background:rgba(26,115,232,0.15);
border-radius:4px;
}
.info-angle{
display: flex;
align-items: center;
}
.angle-value{
flex:1;
color:#5f6368;
font-family:monospace;
font-size:14px;
}
.tiandi-map-container {
width: 100%;
height: 100%;
position: relative;
}
.ol-map-full {
width: 100%;
height: 100%;
position: relative;
}
.ol-popup {
pointer-events: auto;
}
.map-topic-panel {
position: absolute;
top: 0px;
right: 1100px;
width: 338px;
height: 320px;
background: linear-gradient(90deg, rgba(10, 41, 88, 0.3) 0%, rgba(20, 49, 90, 0.8) 52%, rgba(14, 33, 61, 0.3) 100%);
border-radius: 12px;
overflow-y: auto;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
z-index: 2000;
padding: 16px 12px 12px 12px;
display: flex;
flex-direction: column;
gap: 8px;
transition: all 0.6s;
}
.map-topic-panel::-webkit-scrollbar {
width: 4px;
background: transparent;
}
.map-topic-panel::-webkit-scrollbar-thumb {
background: rgba(25, 78, 108, 0.5);
border-radius: 2px;
}
.topic-row {
margin-bottom: 6px;
}
.topic-title {
color: #fff;
font-size: 15px;
font-weight: bold;
margin-bottom: 8px;
letter-spacing: 1px;
}
.topic-btns-cols{
display: flex;
flex-direction: column;
gap: 8px;
}
.topic-btns {
display: flex;
flex-direction: row;
gap: 8px;
}
.topic-btn {
width: 72px;
height: 74px;
background: rgba(15, 53, 75, 0.3);
border-radius: 7px;
border: 1px solid rgba(25, 78, 108, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: box-shadow 0.2s;
}
.topic-btn:hover {
box-shadow: 0 0 8px 2px rgba(20, 49, 90, 0.18);
}
.topic-btn-active {
background: rgba(0, 50, 108, 0.8) !important;
border-radius: 7px;
border: 1px solid #00CEFF !important;
}
.topic-btn-icon {
width: 28px;
height: 28px;
background: rgba(53, 155, 255, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 6px;
}
.topic-btn img {
width: 16px;
height: 16px;
display: block;
}
.topic-btn-label {
color: #fff;
font-size: 14px;
text-align: center;
line-height: 1.2;
min-height: 28px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
/* 保证多行和单行按钮视觉对齐 */
}
.topic-btn-label span {
display: block;
margin: 0;
padding: 0;
}
.video-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10; /* 确保在iframe上层 */
cursor: pointer;
overflow: hidden
}
</style>
自定义弹层显示echarts
子组件代码:
<template>
<el-dialog class="video-dialog" title="电导率详情" v-model="showDialog" width="800" :close-on-click-modal="true"
append-to-body @close="handlerClose">
<div class="dialog-wrapper">
<el-form ref="formRef" :model="form" label-width="100px" label-position="left">
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="选择日期">
<date-picker ref="datePicker" v-model="dateRange" @change="search"></date-picker>
</el-form-item>
</el-col>
</el-row>
</el-form>
<HandlerHistoryEcharts v-model:isshowData="showDialog" :tableList="tableList" :data="deviceData.status_dict" :tag="'conductivity'" />
<div class="title-box">
<div class="circle" style="background: #31E2FF;"></div>
<div class="font">基本信息</div>
</div>
<div class="label-row">
<div class="label-long">设备名:{{deviceData.deviceName}}</div>
<div class="label-short">电池电压:{{deviceData.power}}</div>
<div class="label-middle status-row">
<div>状态:</div>
<div class="circle" :style="'background:'+(deviceData.status_dict==='离线' ? '#D8D8D8' : (deviceData.status_dict==='在线' ? '#3DFF00' : (deviceData.status_dict==='维护' ? '#FFA500' : (deviceData.status_dict==='报警' ? '#FF2D2D' : ''))))"></div>
<div>{{deviceData.status_dict}}</div>
</div>
</div>
<div class="label-row">
<div class="label-short">上限:{{deviceData.conductivityUpperLimit}}</div>
<div class="label-short">下限:{{deviceData.conductivityLowerLimit}}</div>
<div class="label-short">rssi:{{deviceData.rssi}}</div>
<div class="label-middle">imei:{{deviceData.imei}}</div>
</div>
</div>
</el-dialog>
</template>
<script setup>
import HandlerHistoryEcharts from "./HandlerHistoryEcharts";
import DatePicker from "./environment/datePicker.vue";
import dayjs from "dayjs";
import {getConductivityToken, setConductivityToken} from "../../../utils/auth";
import axios from "axios";
import {searchConductivityHistory} from "../../../api/screen/map";
const showDialog = ref(false)
const tableList = ref([])
const deviceData = ref({})
const data = ref({})
const dateRange = ref([dayjs().subtract(1, 'day').format('YYYY-MM-DD'),dayjs().format('YYYY-MM-DD')])
const form = ref({
})
function reset(){
form.value = {}
tableList.value = []
deviceData.value = {}
dateRange.value = [dayjs().subtract(1, 'day').format('YYYY-MM-DD'),dayjs().format('YYYY-MM-DD')]
}
function show(data){
reset()
showDialog.value = true
deviceData.value = data
search()
}
// 关闭弹窗
const handlerClose = () => {
showDialog.value = false
reset()
}
async function search() {
//拿电导率缓存
let token = ''
if ([null, undefined, '', 'undefined', 'null'].includes(getConductivityToken())) {
let uuid = dayjs().format('YYYYMMDD') + Math.floor(Math.random() * 100000000)
let tokenRes = await axios.request({
url: `http://kjgs.jsjnsw.com:17001/jwtauth/api/auth/getToken?uuid=${uuid}`
})
token = tokenRes.data.token
setConductivityToken(token)
} else {
token = getConductivityToken()
}
let query = {
token: token,
imei: deviceData.value.imei,
tag: 'conductivity',
deviceType: '1',
pkConductivity: '',
//当前日期的00:00:00时间戳
startTime: dayjs(dateRange.value[0]) + '',
//当前日期的23:59:59
endTime: dayjs(dateRange.value[1]) + '',
}
let response = await searchConductivityHistory(query)
tableList.value = response.data
}
defineExpose({
show
})
</script>
<style scoped lang='scss'>
.dialog-wrapper{
width: 100%;
height: 500px;
background-image: url("@/assets/images/conductivityDialogBg.png");
background-size: 100% 100%;
box-sizing: border-box;
padding: 74px 40px 36px 42px;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.title-box{
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
}
.status-row{
display: flex;
flex-direction: row;
align-items: center;
}
.circle{
width: 6px;
height: 6px;
border-radius: 6px;
margin-right: 10px;
}
.font{
font-weight: 500;
font-size: 16px;
color: #D8F0FF;
}
.label-row{
margin-top: 10px;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.label-long{
width: 310px;
height: 30px;
background: rgba(11,78,140,0.5);
box-sizing: border-box;
padding-left: 10px;
font-weight: 500;
font-size: 14px;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: flex-start;
}
.label-middle{
width: 238px;
height: 30px;
background: rgba(11,78,140,0.5);
box-sizing: border-box;
padding-left: 10px;
font-weight: 500;
font-size: 14px;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: flex-start;
}
.label-short{
width: 150px;
height: 30px;
background: rgba(11,78,140,0.5);
box-sizing: border-box;
padding-left: 10px;
font-weight: 500;
font-size: 14px;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: flex-start;
}
:deep(.el-form-item__label) {
color: #FFFFFF !important;
}
</style>