|
@@ -1,3 +1,26 @@
|
|
|
|
|
+<!--
|
|
|
|
|
+ Gis.vue - GIS 地图展示页面
|
|
|
|
|
+
|
|
|
|
|
+ 功能说明:
|
|
|
|
|
+ 基于天地图 API 的太湖流域 GIS 数据可视化展示。
|
|
|
|
|
+ 1. 地图核心:
|
|
|
|
|
+ - 初始化天地图底图(EPSG:4326 投影),默认中心点 (120.9, 31.2),缩放级别 9
|
|
|
|
|
+ - 绘制太湖区域边界(tba_area_paths 多边形)
|
|
|
|
|
+ - 鼠标移动实时显示经纬坐标
|
|
|
|
|
+ 2. 子导航栏(GisSubNav):
|
|
|
|
|
+ - 单位选择下拉(切换数据来源单位)
|
|
|
|
|
+ - 业务类型标签(水文监测 / 省界河流 / 省界湖泊 / 水源地 / 入湖河道 / 太湖藻类 / 水质自动站...)
|
|
|
|
|
+ - 地图类型切换(天地图 / 影像图 / 地形图)
|
|
|
|
|
+ 3. 右侧数据面板(GisRightPanel):
|
|
|
|
|
+ - 站名搜索
|
|
|
|
|
+ - 站点数据表格(列根据当前 bizCode 动态变化)
|
|
|
|
|
+ - 点击表格行可定位地图至该站点
|
|
|
|
|
+ 4. 地图标记:
|
|
|
|
|
+ - 根据 bizCode 加载站点数据,在地图上绘制标记点(swjc 图标)
|
|
|
|
|
+ - 点击标记弹出信息窗(含 pH/溶解氧/氨氮/总磷/总氮/水温等水质指标)
|
|
|
|
|
+ - 5分钟数据缓存,避免重复请求
|
|
|
|
|
+ 5. 交互:点击地图空白区域关闭弹窗
|
|
|
|
|
+-->
|
|
|
<script setup>
|
|
<script setup>
|
|
|
import { ref, watch, onMounted, nextTick } from 'vue'
|
|
import { ref, watch, onMounted, nextTick } from 'vue'
|
|
|
import GisSubNav from '@/components/GisSubNav.vue'
|
|
import GisSubNav from '@/components/GisSubNav.vue'
|
|
@@ -9,94 +32,73 @@ import swjcIcon from '@/assets/images/gis/swjc.png'
|
|
|
const TIANDITU_KEY = '3fb1e9fda20ee995dc815c8243553ce8'
|
|
const TIANDITU_KEY = '3fb1e9fda20ee995dc815c8243553ce8'
|
|
|
|
|
|
|
|
const mapLoaded = ref(false)
|
|
const mapLoaded = ref(false)
|
|
|
-const mapContainer = ref(null)
|
|
|
|
|
-let mapInstance = null
|
|
|
|
|
|
|
+const mapContainer = ref(null) // 地图容器 DOM 引用
|
|
|
|
|
+let mapInstance = null // 天地图实例
|
|
|
|
|
|
|
|
-const currentDistrict = ref('unit0')
|
|
|
|
|
-const currentBizCode = ref('TBA_SW_1H2H')
|
|
|
|
|
|
|
+const currentDistrict = ref('unit0') // 当前单位
|
|
|
|
|
+const currentBizCode = ref('TBA_SW_1H2H') // 当前业务代码(默认水文监测)
|
|
|
|
|
|
|
|
-const currentLng = ref('')
|
|
|
|
|
-const currentLat = ref('')
|
|
|
|
|
|
|
+const currentLng = ref('') // 鼠标当前经度
|
|
|
|
|
+const currentLat = ref('') // 鼠标当前纬度
|
|
|
|
|
|
|
|
-const stationData = ref([])
|
|
|
|
|
|
|
+const stationData = ref([]) // 站点数据
|
|
|
|
|
|
|
|
-// 存储当前地图上的标记和图层
|
|
|
|
|
-let markerLayer = null
|
|
|
|
|
-// 当前打开弹窗的标记
|
|
|
|
|
-let activeMarker = null
|
|
|
|
|
|
|
+let markerLayer = null // 当前标记图层
|
|
|
|
|
+let activeMarker = null // 当前打开的弹窗标记
|
|
|
|
|
|
|
|
-// 数据缓存:避免重复请求,5分钟内有效
|
|
|
|
|
|
|
+// 数据缓存:5分钟内避免重复请求
|
|
|
const dataCache = {}
|
|
const dataCache = {}
|
|
|
-const CACHE_TTL = 5 * 60 * 1000 // 5分钟
|
|
|
|
|
|
|
+const CACHE_TTL = 5 * 60 * 1000
|
|
|
|
|
|
|
|
function getCachedData(bizCode) {
|
|
function getCachedData(bizCode) {
|
|
|
const cached = dataCache[bizCode]
|
|
const cached = dataCache[bizCode]
|
|
|
- if (cached && Date.now() - cached.time < CACHE_TTL) {
|
|
|
|
|
- console.log(`[缓存命中] ${bizCode},使用缓存数据`)
|
|
|
|
|
- return cached.data
|
|
|
|
|
- }
|
|
|
|
|
- console.log(`[缓存未命中] ${bizCode}`)
|
|
|
|
|
|
|
+ if (cached && Date.now() - cached.time < CACHE_TTL) return cached.data
|
|
|
return null
|
|
return null
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function setCachedData(bizCode, data) {
|
|
function setCachedData(bizCode, data) {
|
|
|
- // 存一份浅拷贝,避免引用问题
|
|
|
|
|
dataCache[bizCode] = { data: [...data], time: Date.now() }
|
|
dataCache[bizCode] = { data: [...data], time: Date.now() }
|
|
|
- console.log(`[缓存已设置] ${bizCode},${data.length}条数据`)
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 子导航栏事件:选择业务类型
|
|
|
function onSelectBiz({ bizCode, unit }) {
|
|
function onSelectBiz({ bizCode, unit }) {
|
|
|
currentBizCode.value = bizCode
|
|
currentBizCode.value = bizCode
|
|
|
currentDistrict.value = unit
|
|
currentDistrict.value = unit
|
|
|
loadBizData(bizCode)
|
|
loadBizData(bizCode)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function onSearch(keyword) {
|
|
|
|
|
- console.log('Search:', keyword)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function onDistrictChange(unit) {
|
|
|
|
|
- currentDistrict.value = unit
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function onSwitchMapType(type) {
|
|
|
|
|
- console.log('Switch map type:', type)
|
|
|
|
|
-}
|
|
|
|
|
|
|
+function onSearch(keyword) { console.log('Search:', keyword) }
|
|
|
|
|
+function onDistrictChange(unit) { currentDistrict.value = unit }
|
|
|
|
|
+function onSwitchMapType(type) { console.log('Switch map type:', type) }
|
|
|
|
|
|
|
|
|
|
+// 加载站点数据(带缓存)
|
|
|
async function loadBizData(bizCode) {
|
|
async function loadBizData(bizCode) {
|
|
|
- // 优先从缓存读取
|
|
|
|
|
const cached = getCachedData(bizCode)
|
|
const cached = getCachedData(bizCode)
|
|
|
if (cached) {
|
|
if (cached) {
|
|
|
stationData.value = cached
|
|
stationData.value = cached
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 使用数据服务获取数据
|
|
|
|
|
const result = await dataService.getStationData(bizCode)
|
|
const result = await dataService.getStationData(bizCode)
|
|
|
const data = result.rows || []
|
|
const data = result.rows || []
|
|
|
- console.log(`[数据服务] ${bizCode},${data.length}条`)
|
|
|
|
|
stationData.value = data
|
|
stationData.value = data
|
|
|
setCachedData(bizCode, data)
|
|
setCachedData(bizCode, data)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 清除地图上所有标记
|
|
|
function clearMarkers() {
|
|
function clearMarkers() {
|
|
|
- // 关闭当前打开的弹窗
|
|
|
|
|
- if (activeMarker) {
|
|
|
|
|
- activeMarker.closeInfoWindow()
|
|
|
|
|
- activeMarker = null
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (activeMarker) { activeMarker.closeInfoWindow(); activeMarker = null }
|
|
|
if (markerLayer && markerLayer.length > 0) {
|
|
if (markerLayer && markerLayer.length > 0) {
|
|
|
markerLayer.forEach(m => mapInstance.removeOverLay(m))
|
|
markerLayer.forEach(m => mapInstance.removeOverLay(m))
|
|
|
markerLayer = null
|
|
markerLayer = null
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 在地图上添加站点标记
|
|
|
function addMarkers() {
|
|
function addMarkers() {
|
|
|
if (!mapInstance || !stationData.value || stationData.value.length === 0) return
|
|
if (!mapInstance || !stationData.value || stationData.value.length === 0) return
|
|
|
-
|
|
|
|
|
clearMarkers()
|
|
clearMarkers()
|
|
|
-
|
|
|
|
|
markerLayer = []
|
|
markerLayer = []
|
|
|
|
|
+
|
|
|
stationData.value.forEach(item => {
|
|
stationData.value.forEach(item => {
|
|
|
const lng = parseFloat(item.lgtd)
|
|
const lng = parseFloat(item.lgtd)
|
|
|
const lat = parseFloat(item.lttd)
|
|
const lat = parseFloat(item.lttd)
|
|
@@ -109,48 +111,37 @@ function addMarkers() {
|
|
|
iconAnchor: new T.Point(16, 16)
|
|
iconAnchor: new T.Point(16, 16)
|
|
|
})
|
|
})
|
|
|
const marker = new T.Marker(lnglat, { icon: icon })
|
|
const marker = new T.Marker(lnglat, { icon: icon })
|
|
|
|
|
+ // 点击标记显示信息弹窗(动态渲染所有字段)
|
|
|
marker.addEventListener('click', () => {
|
|
marker.addEventListener('click', () => {
|
|
|
- // 动态显示全部字段信息
|
|
|
|
|
const excludeFields = ['stcd', 'lgtd', 'lttd', 'id']
|
|
const excludeFields = ['stcd', 'lgtd', 'lttd', 'id']
|
|
|
let html = `<div style="font-size:13px;line-height:1.8;min-width:150px;">`
|
|
let html = `<div style="font-size:13px;line-height:1.8;min-width:150px;">`
|
|
|
html += `<b>${item.stnm || '未知站点'}</b><hr style="margin:4px 0;border:none;border-top:1px solid #ddd;">`
|
|
html += `<b>${item.stnm || '未知站点'}</b><hr style="margin:4px 0;border:none;border-top:1px solid #ddd;">`
|
|
|
|
|
+ // 字段中文名映射
|
|
|
|
|
+ const labelMap = {
|
|
|
|
|
+ rvnm: '河流名称', tm: '时间', upz: '水位(m)', q: '流量(m³/s)',
|
|
|
|
|
+ spt: '时间', state: '状态', wt: '水温(℃)', ph: 'pH值',
|
|
|
|
|
+ dox: '溶解氧(mg/L)', codmn: '高锰酸盐指数(mg/L)',
|
|
|
|
|
+ nh3n: '氨氮(mg/L)', tp: '总磷(mg/L)', tn: '总氮(mg/L)',
|
|
|
|
|
+ cond: '电导率', admin: '行政区', yr: '年份', turb: '浊度',
|
|
|
|
|
+ chla: '叶绿素a', lzzk: '蓝藻状况', eco: '生态流量'
|
|
|
|
|
+ }
|
|
|
for (const key in item) {
|
|
for (const key in item) {
|
|
|
if (excludeFields.includes(key) || item[key] === '' || item[key] === null || item[key] === undefined) continue
|
|
if (excludeFields.includes(key) || item[key] === '' || item[key] === null || item[key] === undefined) continue
|
|
|
- const labelMap = {
|
|
|
|
|
- rvnm: '河流名称', tm: '时间', upz: '水位(m)', q: '流量(m³/s)',
|
|
|
|
|
- spt: '时间', state: '状态', wt: '水温(℃)', ph: 'pH值',
|
|
|
|
|
- dox: '溶解氧(mg/L)', codmn: '高锰酸盐指数(mg/L)',
|
|
|
|
|
- nh3n: '氨氮(mg/L)', tp: '总磷(mg/L)', tn: '总氮(mg/L)',
|
|
|
|
|
- cod: 'COD', nh3: '氨氮', ysl: '引水量', psl: '排水量',
|
|
|
|
|
- dwz: '闸下水位(m)', gcdl: '调度指令', yxzt: '运行状态',
|
|
|
|
|
- nt: '备注', chla: '叶绿素a', lzzk: '蓝藻状况',
|
|
|
|
|
- cond: '电导率', admin: '行政区', yr: '年份',
|
|
|
|
|
- p_total: '总磷', n_total: '总氮', codcr: '化学需氧量',
|
|
|
|
|
- bod5: '五日生化需氧量', cn: '氰化物', f: '氟化物',
|
|
|
|
|
- s2: '硫化物', las: '阴离子表面活性剂', hg: '汞',
|
|
|
|
|
- cu: '铜', pb: '铅', cd: '镉', zn: '锌', ars: '砷',
|
|
|
|
|
- se: '硒', cr6: '六价铬', oil: '石油类', vlph: '挥发酚',
|
|
|
|
|
- clarity: '透明度', shuise: '水色', wndv: '风速',
|
|
|
|
|
- wnddir: '风向', cyano: '蓝藻密度( cells/L)',
|
|
|
|
|
- zf: '藻毒素', ws: '风速(m/s)', ww: '风向',
|
|
|
|
|
- turb: '浊度', eco: '生态流量'
|
|
|
|
|
- }
|
|
|
|
|
const label = labelMap[key] || key
|
|
const label = labelMap[key] || key
|
|
|
html += `<div style="display:flex;justify-content:space-between;"><span style="color:#666;">${label}:</span><span>${item[key]}</span></div>`
|
|
html += `<div style="display:flex;justify-content:space-between;"><span style="color:#666;">${label}:</span><span>${item[key]}</span></div>`
|
|
|
}
|
|
}
|
|
|
html += `</div>`
|
|
html += `</div>`
|
|
|
- // 关闭上一个弹窗
|
|
|
|
|
if (activeMarker) activeMarker.closeInfoWindow()
|
|
if (activeMarker) activeMarker.closeInfoWindow()
|
|
|
activeMarker = marker
|
|
activeMarker = marker
|
|
|
- marker.openInfoWindow(new T.InfoWindow(html, {offsetX: -10, offsetY: -30}))
|
|
|
|
|
|
|
+ marker.openInfoWindow(new T.InfoWindow(html, { offsetX: -10, offsetY: -30 }))
|
|
|
})
|
|
})
|
|
|
mapInstance.addOverLay(marker)
|
|
mapInstance.addOverLay(marker)
|
|
|
markerLayer.push(marker)
|
|
markerLayer.push(marker)
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 点击表格行:定位地图至该站点
|
|
|
function onRowClick(row) {
|
|
function onRowClick(row) {
|
|
|
- // 点击表格行,地图定位到该站点
|
|
|
|
|
const lng = parseFloat(row.lgtd)
|
|
const lng = parseFloat(row.lgtd)
|
|
|
const lat = parseFloat(row.lttd)
|
|
const lat = parseFloat(row.lttd)
|
|
|
if (!isNaN(lng) && !isNaN(lat) && mapInstance) {
|
|
if (!isNaN(lng) && !isNaN(lat) && mapInstance) {
|
|
@@ -158,41 +149,37 @@ function onRowClick(row) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 监听 stationData 变化,刷新地图标记
|
|
|
|
|
|
|
+// 监听站点数据变化,刷新地图标记
|
|
|
watch(stationData, () => {
|
|
watch(stationData, () => {
|
|
|
- if (mapInstance) {
|
|
|
|
|
- addMarkers()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (mapInstance) addMarkers()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// 初始化天地图
|
|
|
function initMap() {
|
|
function initMap() {
|
|
|
if (typeof T !== 'undefined' && mapContainer.value) {
|
|
if (typeof T !== 'undefined' && mapContainer.value) {
|
|
|
- const center = new T.LngLat(120.9, 31.2)
|
|
|
|
|
|
|
+ const center = new T.LngLat(120.9, 31.2) // 太湖流域地理中心
|
|
|
mapInstance = new T.Map(mapContainer.value, {
|
|
mapInstance = new T.Map(mapContainer.value, {
|
|
|
center: center,
|
|
center: center,
|
|
|
zoom: 9,
|
|
zoom: 9,
|
|
|
- projection: 'EPSG:4326'
|
|
|
|
|
|
|
+ projection: 'EPSG:4326' // WGS84 经纬度投影
|
|
|
})
|
|
})
|
|
|
const tileLayer = new T.TileLayer()
|
|
const tileLayer = new T.TileLayer()
|
|
|
mapInstance.addLayer(tileLayer)
|
|
mapInstance.addLayer(tileLayer)
|
|
|
mapLoaded.value = true
|
|
mapLoaded.value = true
|
|
|
|
|
|
|
|
- drawTaihuBoundary()
|
|
|
|
|
|
|
+ drawTaihuBoundary() // 绘制太湖边界
|
|
|
|
|
|
|
|
|
|
+ // 鼠标移动:实时更新经纬度显示
|
|
|
mapInstance.addEventListener('mousemove', (e) => {
|
|
mapInstance.addEventListener('mousemove', (e) => {
|
|
|
if (e.lnglat) {
|
|
if (e.lnglat) {
|
|
|
currentLng.value = e.lnglat.lng.toFixed(4)
|
|
currentLng.value = e.lnglat.lng.toFixed(4)
|
|
|
currentLat.value = e.lnglat.lat.toFixed(4)
|
|
currentLat.value = e.lnglat.lat.toFixed(4)
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
- // 点击地图空白区域关闭弹窗
|
|
|
|
|
|
|
+ // 点击地图空白区关闭弹窗
|
|
|
mapInstance.addEventListener('click', (e) => {
|
|
mapInstance.addEventListener('click', (e) => {
|
|
|
- // 仅当点击的不是标记时关闭
|
|
|
|
|
if (!e.target || !(e.target._marker || e.target.classList?.contains('tdt-marker'))) {
|
|
if (!e.target || !(e.target._marker || e.target.classList?.contains('tdt-marker'))) {
|
|
|
- if (activeMarker) {
|
|
|
|
|
- activeMarker.closeInfoWindow()
|
|
|
|
|
- activeMarker = null
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (activeMarker) { activeMarker.closeInfoWindow(); activeMarker = null }
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|