diff --git a/packages/screen/src/views/cockpit/api/commonHttp.js b/packages/screen/src/views/cockpit/api/commonHttp.js new file mode 100644 index 0000000..3c4ec8c --- /dev/null +++ b/packages/screen/src/views/cockpit/api/commonHttp.js @@ -0,0 +1,12 @@ +import { request } from '@shared/utils/request' + +// 获取业务基础地图 +export function getBusinessBaseMapLayer() { + return request({ + url: '/snow-ops-platform/dataDirectory/queryCatalog', + method: 'GET', + params: { + pcatalog: 'DDT' + } + }) +} \ No newline at end of file diff --git a/packages/screen/src/views/cockpit/assets/legendTool/气象预警icon定位.png b/packages/screen/src/views/cockpit/assets/legendTool/气象预警icon定位.png new file mode 100644 index 0000000..165d362 Binary files /dev/null and b/packages/screen/src/views/cockpit/assets/legendTool/气象预警icon定位.png differ diff --git a/packages/screen/src/views/cockpit/components/CockpitLayout.vue b/packages/screen/src/views/cockpit/components/CockpitLayout.vue index 4d518c7..36beaf8 100644 --- a/packages/screen/src/views/cockpit/components/CockpitLayout.vue +++ b/packages/screen/src/views/cockpit/components/CockpitLayout.vue @@ -26,7 +26,7 @@ - + + + diff --git a/packages/screen/src/views/cockpit/components/ImageMarkTooltip/blockEvent.vue b/packages/screen/src/views/cockpit/components/ImageMarkTooltip/blockEvent.vue new file mode 100644 index 0000000..2aa8a1e --- /dev/null +++ b/packages/screen/src/views/cockpit/components/ImageMarkTooltip/blockEvent.vue @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/packages/screen/src/views/cockpit/components/ImageMarkTooltip/index.js b/packages/screen/src/views/cockpit/components/ImageMarkTooltip/index.js new file mode 100644 index 0000000..ab102aa --- /dev/null +++ b/packages/screen/src/views/cockpit/components/ImageMarkTooltip/index.js @@ -0,0 +1,78 @@ +import { createVNode, render } from 'vue' +import ImageMarkTooltip from './ImageMarkTooltip.vue' + +class ImageMarkTooltipUI { + constructor() { + this.instance = null + this.container = null + this.entity = null + this.root = null + } + + // 显示 tooltip + show(options = {}) { + // 销毁之前的实例 + this.close() + + // 创建容器 + this.container = document.createElement('div') + this.container.className = 'tooltip-service-container' + this.root = document.querySelector('.cockpit-main') + this.root.appendChild(this.container) + + this.entity = options.entity + + // 创建 VNode + const vnode = createVNode(ImageMarkTooltip, { + visible: true, + position: options.position || { x: 0, y: 0 }, + data: options.data || {}, + loading: options.loading || false, + error: options.error || '', + onClose: () => { + this.close() + } + }) + + // 渲染到容器 + render(vnode, this.container) + this.instance = vnode.component + return this.instance + } + + updatePosition(position) { + if (this.instance) { + this.instance.props.position = position + this.instance.update() + } + } + + // 更新 tooltip 内容 + update(options) { + if (this.instance) { + this.instance.props = { + ...this.instance.props, + ...options + } + } + } + + // 关闭 tooltip + close() { + if (this.container) { + render(null, this.container) + this.root.removeChild(this.container) + this.container = null + this.instance = null + } + } +} + +// 创建单例实例 +const instance = new ImageMarkTooltipUI() + +export const CommonTooltip = instance + +export const newImageMarkTooltip = () => { + return new ImageMarkTooltipUI() +} \ No newline at end of file diff --git a/packages/screen/src/views/cockpit/components/ImageMarkTooltip/riskRoad.vue b/packages/screen/src/views/cockpit/components/ImageMarkTooltip/riskRoad.vue new file mode 100644 index 0000000..0dde344 --- /dev/null +++ b/packages/screen/src/views/cockpit/components/ImageMarkTooltip/riskRoad.vue @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/packages/screen/src/views/cockpit/components/ImageMarkTooltip/serviceFacility.vue b/packages/screen/src/views/cockpit/components/ImageMarkTooltip/serviceFacility.vue new file mode 100644 index 0000000..c8cd7e7 --- /dev/null +++ b/packages/screen/src/views/cockpit/components/ImageMarkTooltip/serviceFacility.vue @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/packages/screen/src/views/cockpit/components/ImageMarkTooltip/weatherAlert.vue b/packages/screen/src/views/cockpit/components/ImageMarkTooltip/weatherAlert.vue new file mode 100644 index 0000000..2aa8a1e --- /dev/null +++ b/packages/screen/src/views/cockpit/components/ImageMarkTooltip/weatherAlert.vue @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/packages/screen/src/views/cockpit/components/LegendToolbar.vue b/packages/screen/src/views/cockpit/components/LegendToolbar.vue index da66089..26bd9b7 100644 --- a/packages/screen/src/views/cockpit/components/LegendToolbar.vue +++ b/packages/screen/src/views/cockpit/components/LegendToolbar.vue @@ -76,6 +76,7 @@ import blockEventMarkerIcon from '../assets/legendTool/阻断事件icon定位.pn import emergencyForceIcon from '../assets/legendTool/应急力量icon.png' import emergencyForceMarkerIcon from '../assets/legendTool/应急力量icon定位.png' import weatherAlertIcon from '../assets/legendTool/气象预警icon.png' +import weatherAlertMarkerIcon from '../assets/legendTool/气象预警icon定位.png' // 默认图例项配置(包含普通图标和地图定位图标) const defaultLegendItems = [ @@ -84,12 +85,12 @@ const defaultLegendItems = [ { key: 'trafficMonitor', label: '交调', icon: trafficMonitorIcon, markerIcon: trafficMonitorMarkerIcon }, { key: 'bridge', label: '桥梁', icon: bridgeIcon, markerIcon: bridgeMarkerIcon }, { key: 'tunnel', label: '隧洞', icon: tunnelIcon, markerIcon: tunnelMarkerIcon }, - { key: 'serviceFacility', label: '服务设施', icon: serviceFacilityIcon, markerIcon: serviceFacilityMarkerIcon }, - { key: 'riskRoad', label: '风险路段', icon: riskRoadIcon, markerIcon: riskRoadMarkerIcon }, + { key: 'serviceFacility', label: '养护站', icon: serviceFacilityIcon, markerIcon: serviceFacilityMarkerIcon }, + { key: 'riskRoad', label: '高海拔路段', icon: riskRoadIcon, markerIcon: riskRoadMarkerIcon }, { key: 'hazardPoint', label: '涉灾隐患点', icon: hazardPointIcon, markerIcon: hazardPointMarkerIcon }, { key: 'blockEvent', label: '阻断事件', icon: blockEventIcon, markerIcon: blockEventMarkerIcon }, { key: 'emergencyForce', label: '应急力量', icon: emergencyForceIcon, markerIcon: emergencyForceMarkerIcon }, - { key: 'weatherAlert', label: '气象预警', icon: weatherAlertIcon, markerIcon: weatherAlertIcon } + { key: 'weatherAlert', label: '气象预警', icon: weatherAlertIcon, markerIcon: weatherAlertMarkerIcon } ] // 定义 props @@ -103,6 +104,14 @@ const props = defineProps({ default: null }, + /** + * 根据key显示对应图例 + */ + legendKeys: { + type: Array, + default: null + }, + /** * Marker 数据配置(可选) * 如果提供,将在事件中传递完整的 marker 数据 @@ -135,10 +144,10 @@ const props = defineProps({ * 兼容旧版:标记点切换回调函数 * @deprecated 推荐使用 @marker-toggle 事件 */ - onMarkerToggle: { - type: Function, - default: null - } + // onMarkerToggle: { + // type: Function, + // default: null + // } }) // 定义 emits @@ -146,6 +155,13 @@ const emit = defineEmits(['marker-toggle', 'clear', 'update:modelValue', 'collap // 计算图例项配置(支持自定义) const computedLegendItems = computed(() => { + if(props.legendKeys && props.legendKeys.length > 0) { + return props.legendKeys.map(key => { + const item = defaultLegendItems.find(item => item.key === key) + return item + }) + } + return props.legendItems && props.legendItems.length > 0 ? props.legendItems : defaultLegendItems @@ -260,7 +276,6 @@ const handleItemClick = (key) => { // 构建并发送事件 const payload = buildPayload(key, newIsActive) emit('marker-toggle', payload) - // 兼容旧版 props 回调 if (props.onMarkerToggle) { props.onMarkerToggle( diff --git a/packages/screen/src/views/cockpit/composables/ImageMarkData.js b/packages/screen/src/views/cockpit/composables/ImageMarkData.js new file mode 100644 index 0000000..128c613 --- /dev/null +++ b/packages/screen/src/views/cockpit/composables/ImageMarkData.js @@ -0,0 +1,99 @@ +import { newImageMarkTooltip } from '../components/ImageMarkTooltip' + +// 专门用于绘制图片的api数据类 +export default class ImageMarkData { + key = null + markerIcon = null + api = null + // 提示框实例,当鼠标移入或者点击图例时,需要获取到该实例 + tooltip = null + onResponse = null + cacheData = null + expiresAt = null + abortController = null + /** + * 60秒内重复点击使用缓存数据,减少服务器压力 + */ + cacheTime = 60 * 1000 + + constructor({ api, tooltip, onResponse }) { + this.api = api + this.response = onResponse + // 初始化提示框 + this.tooltip = tooltip || newImageMarkTooltip() + } + + getCache = () => { + // 检查缓存是否有效 + if ( + this.cacheData?.length && + this.expiresAt > Date.now() + ) { + return this.cacheData + } + } + + request = async (params) => { + // 首先查看缓存是否可用 + const cacheData = this.getCache() + if (cacheData) return cacheData + + const controller = this.createAbortController() + const config = {} + if (controller) config.signal = controller.signal + let res = null + try { + res = await this.api(params, config) + } catch (error) { + console.error('请求失败: ', error) + } finally { + let data = null + if (this.response) data = this.response(res) + else data = this.commonOnResponse(res) + + this.cacheData = data + this.expiresAt = Date.now() + this.cacheTime + + return data + } + } + + createAbortController = () => { + if (typeof AbortController === 'undefined') { + console.warn('当前环境不支持 AbortController') + return null + } + return new AbortController() + } + + cancelRequest = () => { + if (this.abortController) { + try { + this.abortController.abort() + } catch (error) { + this.console.warn('取消请求失败', error) + } finally { + this.abortController = null + } + } + } + + // 通用的响应处理 + commonOnResponse = (res) => { + if (res?.success) { + res.data = res.data.slice(0, 2) + const dataList = res.data.map((item) => { + item.mapData = { + id: this.key + '-' + item.rid, + layerId: this.key, + position: [item.jd, item.wd, 0], + image: this.markerIcon + } + return item + }) + this.data = dataList + return dataList + } + return [] + } +} \ No newline at end of file diff --git a/packages/screen/src/views/cockpit/composables/useMapBase.js b/packages/screen/src/views/cockpit/composables/useMapBase.js new file mode 100644 index 0000000..2983faf --- /dev/null +++ b/packages/screen/src/views/cockpit/composables/useMapBase.js @@ -0,0 +1,37 @@ +import { getBusinessBaseMapLayer } from '@/views/cockpit/api/commonHttp.js' + +// 当前页面的最基础地图服务 +// 主要是加载地图底图 +export const useMapBase = (mapStore) => { + + const loadBusinessBaseMapLayer = async () => { + const layerService = mapStore.services().layer + const res = await getBusinessBaseMapLayer() + const data = [...res] + mapStore.baseMapGroups = data + for (const item of data) { + const layers = mapStore.getBaseMapLayersForGroup(item.Attribute?.rid || item.Rid) + for (const layerConfig of layers) { + const layer = { + id: layerConfig.id, + type: layerConfig.type, + url: layerConfig.url, + meta: layerConfig.meta, + } + await layerService.addLayer(layer) + } + } + + + } + + const loadBaseData = () => { + setTimeout(() => { + loadBusinessBaseMapLayer() + }, 0) + } + + return { + loadBaseData + } +} \ No newline at end of file diff --git a/packages/screen/src/views/cockpit/composables/useMapImageMark.js b/packages/screen/src/views/cockpit/composables/useMapImageMark.js new file mode 100644 index 0000000..e3f3912 --- /dev/null +++ b/packages/screen/src/views/cockpit/composables/useMapImageMark.js @@ -0,0 +1,177 @@ +import { fetchEmergencyForceList } from '@/views/cockpit/api/emergencyForce' + +import ImageMarkData from './ImageMarkData' +import * as Cesium from 'cesium' + +/** + * 当前业务下的地图服务 + * 主要是涉及需要调用后端服务获得地图数据 + * */ + +export const useMapImageMark = (mapStore) => { + + // mapStore准备就绪进行初始化 + mapStore.onReady(() => init()) + + // 接口服务映射 + const imageMarkMap = { + serviceFacility: new ImageMarkData({ + api: fetchEmergencyForceList, + }), + riskRoad: new ImageMarkData({ + api: fetchEmergencyForceList, + }), + blockEvent: new ImageMarkData({ + api: fetchEmergencyForceList, + }), + weatherAlert: new ImageMarkData({ + api: fetchEmergencyForceList, + }) + } + + let entityService + let viewer + let handler + + const init = () => { + entityService = mapStore.services().entity + viewer = mapStore.getViewer() + + // 事件注册 + handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas); + + // 点击事件 + handler.setInputAction(function (event) { + const pickedFeature = viewer.scene.pick(event.position); + if (pickedFeature?.id) { + const entity = pickedFeature.id + const data = entity.properties.originalData.getValue() + const imageMarkData = imageMarkMap[data.mapData.layerId] + imageMarkData.tooltip.show({ + data, + entity, + position: getScreenPosition(entity) + }) + } + + }, Cesium.ScreenSpaceEventType.LEFT_CLICK); + + // 帧渲染函数,也就是显示器的每一帧都会执行 + // 地图拖动与放大的监控,需要更新各个tooltips的位置, 通过消息来通知 + viewer.scene.postRender.addEventListener(() => { + for(const key in imageMarkMap) { + const imageMarkData = imageMarkMap[key] + const entity = imageMarkData.tooltip.entity + if(!entity) continue + imageMarkData.tooltip.updatePosition(getScreenPosition(entity)) + } + }) + } + + + + /** + * 绘制各个地图标,例如点击LengendItem的某一项,就会显示对应的图标列表 + */ + const drawImageEntities = async (dataList) => { + for (const item of dataList) { + const mapData = item.mapData + entityService.addBillboard({ + id: mapData.id, + layerId: mapData.layerId, + position: mapData.position, + image: mapData.image, + width: mapData.width || 44, + height: mapData.height || 44, + clampToGround: true, + properties: { originalData: item } + }) + } + } + + /* + * 清除某个图层里的实体 + */ + const clearLayerEntity = (layerId) => { + entityService.clearLayerEntities(layerId) + const imageMarkData = imageMarkMap[layerId] + if (!imageMarkData) return + imageMarkData.tooltip.close() + } + + /** + * 根据图标名称请求后台服务,动态加载图标列表 + */ + const loadDynamicMark = async ({ key, markerIcon, params }) => { + const imageMarkData = imageMarkMap[key] + if (!imageMarkData) return + imageMarkData.markerIcon = markerIcon + imageMarkData.key = key + const dataList = await imageMarkData.request(params) + + drawImageEntities(dataList) + } + + /** + * 显示/隐藏地图标 + */ + const toggleMark = async ({ key, active, markerIcon, params }) => { + const imageMarkData = imageMarkMap[key] + + if (!active) { + clearLayerEntity(key) + if (imageMarkData) imageMarkData.cancelRequest() + return + } + + loadDynamicMark({ key, markerIcon, params }) + } + + /** + * 获得该实体位于屏幕的位置,用于html元素进行定位使用 + */ + const getScreenPosition = (entity) => { + + try { + const position = entity.position?.getValue(Cesium.JulianDate.now()) + if (!position) return + + // 处理 clampToGround 实体 + // 对于使用 CLAMP_TO_GROUND 的 billboard,需要获取实际的地形高度 + let clampedPosition = position + + // 转换为地理坐标 + const cartographic = Cesium.Cartographic.fromCartesian(position) + + // 尝试获取地形高度 + if (viewer.scene.globe) { + const height = viewer.scene.globe.getHeight(cartographic) + + // 如果成功获取地形高度,使用地形高度重建位置 + if (Cesium.defined(height)) { + cartographic.height = height + clampedPosition = Cesium.Cartographic.toCartesian(cartographic) + } else { + // 备用方案:尝试使用 clampToHeight + const clampedCartesian = viewer.scene.clampToHeight(position) + if (clampedCartesian) { + clampedPosition = clampedCartesian + } + } + } + + // 使用修正后的位置进行屏幕坐标转换 + const screenPosition = viewer.scene.cartesianToCanvasCoordinates(clampedPosition) + if (screenPosition) { + return screenPosition + } + } catch (error) { + console.error('更新 Tooltip 位置失败:', error) + } + } + + return { + toggleMark + } + +} \ No newline at end of file