| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090 |
- <template>
- <div class="gis-dashboard">
- <div class="dashboard-header">
- <div class="header-title">
- <div class="title-main">
- <el-icon><MapLocation /></el-icon>
- <span>燃气管网GIS一张图</span>
- </div>
- <div class="title-metrics">
- <span class="metric metric-danger">管线 {{ summaryStats.pipelineLength }}km</span>
- <span class="metric metric-primary">燃气管点 {{ summaryStats.pipePointCount }}个</span>
- <span class="metric metric-warning">窨井 {{ summaryStats.manholeCount }}个</span>
- </div>
- </div>
- <div class="header-actions">
- <el-input
- v-model="searchKeyword"
- class="search-input"
- placeholder="搜索管点/井盖编号/位置"
- clearable
- @keyup.enter="handleSearch"
- >
- <template #prefix>
- <el-icon><Search /></el-icon>
- </template>
- </el-input>
- <el-button type="primary" @click="handleSearch">
- <el-icon><Search /></el-icon>
- 搜索
- </el-button>
- <el-button @click="refreshScene">
- <el-icon><RefreshRight /></el-icon>
- 刷新
- </el-button>
- <el-button @click="toggleFullscreen">
- <el-icon><FullScreen /></el-icon>
- 全图
- </el-button>
- </div>
- </div>
- <div ref="screenRef" class="dashboard-content">
- <aside class="dashboard-sidebar">
- <el-card shadow="never" class="panel-card">
- <template #header>
- <span>图层控制</span>
- </template>
- <div class="layer-group">
- <div class="layer-title">燃气管线</div>
- <el-checkbox v-model="layerState.pipelines.high">高压管线</el-checkbox>
- <el-checkbox v-model="layerState.pipelines.subHigh">次高压管线</el-checkbox>
- <el-checkbox v-model="layerState.pipelines.medium">中压管线</el-checkbox>
- <el-checkbox v-model="layerState.pipelines.low">低压管线</el-checkbox>
- </div>
- <div class="layer-group">
- <div class="layer-title">燃气管点</div>
- <el-checkbox v-model="layerState.pipePoints.valve">阀门</el-checkbox>
- <el-checkbox v-model="layerState.pipePoints.regulator">调压器</el-checkbox>
- <el-checkbox v-model="layerState.pipePoints.flowmeter">流量计</el-checkbox>
- <el-checkbox v-model="layerState.pipePoints.terminal">终端节点</el-checkbox>
- </div>
- <div class="layer-group layer-group-last">
- <div class="layer-title">窨井设施</div>
- <el-checkbox v-model="layerState.manholes.cover">窨井盖</el-checkbox>
- <el-checkbox v-model="layerState.manholes.inspection">检查井</el-checkbox>
- </div>
- </el-card>
- <el-card shadow="never" class="panel-card">
- <template #header>
- <span>图例说明</span>
- </template>
- <div class="legend-list">
- <div class="legend-item"><span class="legend-line legend-high"></span><span>高压管线</span></div>
- <div class="legend-item"><span class="legend-line legend-sub-high"></span><span>次高压管线</span></div>
- <div class="legend-item"><span class="legend-line legend-medium"></span><span>中压管线</span></div>
- <div class="legend-item"><span class="legend-line legend-low"></span><span>低压管线</span></div>
- <div class="legend-item"><span class="legend-dot legend-valve"></span><span>阀门</span></div>
- <div class="legend-item"><span class="legend-dot legend-regulator"></span><span>调压器</span></div>
- <div class="legend-item"><span class="legend-dot legend-flowmeter"></span><span>流量计</span></div>
- <div class="legend-item"><span class="legend-dot legend-terminal"></span><span>终端节点</span></div>
- <div class="legend-item"><span class="legend-square legend-cover"></span><span>窨井盖</span></div>
- <div class="legend-item"><span class="legend-square legend-inspection"></span><span>检查井</span></div>
- </div>
- </el-card>
- <el-card shadow="never" class="panel-card stats-card">
- <template #header>
- <span>测绘数据统计</span>
- </template>
- <div class="stats-list">
- <div class="stats-row"><span>管线总长</span><strong>{{ summaryStats.pipelineLength }} km</strong></div>
- <div class="stats-row"><span>管点总数</span><strong>{{ summaryStats.pipePointCount }}</strong></div>
- <div class="stats-row"><span>窨井总数</span><strong>{{ summaryStats.manholeCount }}</strong></div>
- <div class="stats-row"><span>覆盖面积</span><strong>{{ summaryStats.coverageArea }} km²</strong></div>
- </div>
- </el-card>
- <el-card shadow="never" class="panel-card detail-card">
- <template #header>
- <span>要素详情</span>
- </template>
- <div v-if="selectedFeature" class="detail-body">
- <div class="detail-name">{{ selectedFeature.name }}</div>
- <div class="detail-tag">{{ selectedFeature.category }}</div>
- <el-descriptions :column="1" border size="small">
- <el-descriptions-item
- v-for="item in selectedFeatureDetails"
- :key="item.label"
- :label="item.label"
- >
- {{ item.value }}
- </el-descriptions-item>
- </el-descriptions>
- </div>
- <el-empty v-else description="点击地图要素查看详情" :image-size="72" />
- </el-card>
- </aside>
- <section class="dashboard-map-panel">
- <div class="map-toolbar">
- <div class="toolbar-left">
- <el-button @click="zoomIn">
- <el-icon><Plus /></el-icon>
- 放大
- </el-button>
- <el-button @click="zoomOut">
- <el-icon><Minus /></el-icon>
- 缩小
- </el-button>
- <el-button @click="locateCenter">
- <el-icon><Location /></el-icon>
- 定位
- </el-button>
- </div>
- <div class="toolbar-right">当前坐标: {{ currentCoordinate }}</div>
- </div>
- <div id="gisMap" ref="mapRef" class="gis-map"></div>
- <div v-if="mapError" class="map-error">{{ mapError }}</div>
- </section>
- </div>
- </div>
- </template>
- <script setup>
- import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
- import { FullScreen, Location, MapLocation, Minus, Plus, RefreshRight, Search } from '@element-plus/icons-vue'
- import { ElMessage } from 'element-plus'
- import { convertCoord } from '@/utils/coordTransform'
- import { getPipeNetworkOptions, getGasPipePointPage, getManholeList } from '@/api/pipeNetwork/basic'
- const centerPoint = { lng: 110.386, lat: 28.445 }
- const MAP_BOUNDS = {
- southWest: { lng: 110.355, lat: 28.428 },
- northEast: { lng: 110.410, lat: 28.460 }
- }
- const MAP_MIN_ZOOM = 13
- const MAP_MAX_ZOOM = 19
- const MAP_DEFAULT_ZOOM = 15
- const defaultLayerState = () => ({
- pipelines: { high: true, subHigh: true, medium: true, low: true },
- pipePoints: { valve: true, regulator: true, flowmeter: true, terminal: true },
- manholes: { cover: true, inspection: true }
- })
- const boundaryPoints = [
- { lng: 110.358, lat: 28.456 },
- { lng: 110.375, lat: 28.458 },
- { lng: 110.392, lat: 28.458 },
- { lng: 110.408, lat: 28.455 },
- { lng: 110.410, lat: 28.448 },
- { lng: 110.407, lat: 28.440 },
- { lng: 110.400, lat: 28.436 },
- { lng: 110.390, lat: 28.434 },
- { lng: 110.378, lat: 28.433 },
- { lng: 110.368, lat: 28.436 },
- { lng: 110.360, lat: 28.445 }
- ]
- const pipelineData = [
- {
- id: 'pipe-high-01', level: 'high', name: '燃气1', code: 'GX-HP-001', length: 4.1,
- material: 'L290N 无缝钢管', diameter: 'DN300', pressure: '高压A (4.0MPa)', road: '燃气1主干线',
- status: '运行正常', layingMethod: '直埋敷设', depth: '1.8m', commissioningDate: '2026-06',
- points: [{lng:110.404947,lat:28.433419},{lng:110.404836,lat:28.433429},{lng:110.403275,lat:28.433571},{lng:110.401084,lat:28.433876},{lng:110.399601,lat:28.433876},{lng:110.398112,lat:28.433979},{lng:110.396717,lat:28.434314},{lng:110.394299,lat:28.434538},{lng:110.394352,lat:28.434887},{lng:110.391969,lat:28.434985},{lng:110.384374,lat:28.435695},{lng:110.381418,lat:28.43559},{lng:110.377993,lat:28.436193},{lng:110.376062,lat:28.436385},{lng:110.374231,lat:28.43655},{lng:110.372584,lat:28.437107},{lng:110.371292,lat:28.4377},{lng:110.370581,lat:28.438198},{lng:110.369831,lat:28.438821}]
- },
- {
- id: 'pipe-subhigh-01', level: 'subHigh', name: '燃气2', code: 'GX-SHP-002', length: 3.8,
- material: 'L245N 无缝钢管', diameter: 'DN250', pressure: '次高压A (1.6MPa)', road: '燃气2支线',
- status: '运行正常', layingMethod: '直埋敷设', depth: '1.6m', commissioningDate: '2026-03',
- points: [{lng:110.361433,lat:28.454979},{lng:110.362363,lat:28.453169},{lng:110.362851,lat:28.452213},{lng:110.364377,lat:28.452443},{lng:110.365138,lat:28.45246},{lng:110.366462,lat:28.452157},{lng:110.369549,lat:28.451441},{lng:110.37219,lat:28.450897},{lng:110.372753,lat:28.450901},{lng:110.373875,lat:28.451194},{lng:110.373913,lat:28.451107},{lng:110.374597,lat:28.451476}]
- }
- ]
- const pipePointData = ref([])
- const manholeData = ref([])
- async function loadGisData() {
- try {
- const [pointRes, manholeRes] = await Promise.all([
- getGasPipePointPage(1, 500, {}),
- getManholeList()
- ])
- const points = (pointRes.data && pointRes.data.records) ? pointRes.data.records : []
- pipePointData.value = points.filter(p => p.longitude && p.latitude).map(p => ({
- id: 'point-' + p.pointId, type: mapPointType(p.pointType), pipelineId: p.networkId,
- name: p.pointName, code: p.pointCode, road: p.networkName || '', status: '在线',
- deviceModel: p.material || '', caliber: p.diameter ? p.diameter + 'mm' : '',
- lng: Number(p.longitude), lat: Number(p.latitude)
- }))
- const manholes = manholeRes.data || []
- manholeData.value = manholes.filter(m => m.longitude && m.latitude).map(m => ({
- id: 'manhole-' + m.manholeId, type: 'cover', pipelineId: m.networkId || '',
- name: m.manholeName, code: m.manholeCode, road: '', status: m.status === '0' ? '完好' : '异常',
- material: m.material || '', size: m.manholeShape || '', lng: Number(m.longitude), lat: Number(m.latitude)
- }))
- if (mapInstance && typeof BMapGL !== 'undefined') renderMapScene()
- } catch (e) { console.error(e) }
- }
- function mapPointType(type) {
- if (!type) return 'terminal'
- if (type.includes('阀门') || type === 'valve') return 'valve'
- if (type.includes('调压') || type === 'regulator') return 'regulator'
- if (type.includes('计量') || type.includes('流量') || type === 'flowmeter') return 'flowmeter'
- return 'terminal'
- }
- const colorMap = {
- high: '#e63946',
- subHigh: '#f77f00',
- medium: '#fcbf49',
- low: '#2a9d8f',
- valve: '#457b9d',
- regulator: '#e76f51',
- flowmeter: '#2d6a4f',
- terminal: '#6c757d',
- cover: '#7f4f24',
- inspection: '#b08968'
- }
- const labelMap = {
- high: '高压管线',
- subHigh: '次高压管线',
- medium: '中压管线',
- low: '低压管线',
- valve: '阀门',
- regulator: '调压器',
- flowmeter: '流量计',
- terminal: '终端节点',
- cover: '窨井盖',
- inspection: '检查井'
- }
- const mapRef = ref(null)
- const screenRef = ref(null)
- const searchKeyword = ref('')
- const currentCoordinate = ref(`${centerPoint.lng}, ${centerPoint.lat}`)
- const selectedFeature = ref(null)
- const mapError = ref('')
- const layerState = reactive(defaultLayerState())
- let mapInstance = null
- let activeInfoWindow = null
- let overlayClicked = false
- const summaryStats = computed(() => ({
- pipelineLength: pipelineData.reduce((sum, item) => sum + item.length, 0).toFixed(1),
- pipePointCount: pipePointData.value.length,
- manholeCount: manholeData.value.length,
- coverageArea: '18.6'
- }))
- const selectedFeatureDetails = computed(() => {
- if (!selectedFeature.value) return []
- return Object.entries(selectedFeature.value.details).map(([label, value]) => ({ label, value }))
- })
- const searchableFeatures = computed(() => {
- const pipelines = pipelineData.map(item => ({
- kind: 'pipeline',
- type: item.level,
- name: item.name,
- code: item.code,
- road: item.road,
- lng: getMidPoint(item.points).lng,
- lat: getMidPoint(item.points).lat,
- raw: item
- }))
- const pipePoints = pipePointData.value.map(item => ({
- kind: 'pipePoint',
- type: item.type,
- name: item.name,
- code: item.code,
- road: item.road,
- lng: item.lng,
- lat: item.lat,
- raw: item
- }))
- const manholes = manholeData.value.map(item => ({
- kind: 'manhole',
- type: item.type,
- name: item.name,
- code: item.code,
- road: item.road,
- lng: item.lng,
- lat: item.lat,
- raw: item
- }))
- return [...pipelines, ...pipePoints, ...manholes]
- })
- watch(layerState, () => {
- renderMapScene()
- }, { deep: true })
- onMounted(async () => {
- await nextTick()
- await loadGisData()
- waitForBMapGL()
- document.addEventListener('fullscreenchange', handleFullscreenChange)
- })
- onBeforeUnmount(() => {
- document.removeEventListener('fullscreenchange', handleFullscreenChange)
- if (mapInstance) {
- mapInstance.clearOverlays()
- mapInstance = null
- }
- })
- function waitForBMapGL() {
- if (typeof BMapGL !== 'undefined') {
- initMap()
- return
- }
- let attempts = 0
- const maxAttempts = 50
- const timer = setInterval(() => {
- attempts++
- if (typeof BMapGL !== 'undefined') {
- clearInterval(timer)
- initMap()
- return
- }
- if (attempts >= maxAttempts) {
- clearInterval(timer)
- mapError.value = '百度地图脚本加载超时,请刷新页面重试。'
- }
- }, 200)
- }
- function initMap() {
- const el = document.getElementById('gisMap')
- if (!el) {
- mapError.value = '地图容器未找到。'
- return
- }
- try {
- mapInstance = new BMapGL.Map('gisMap', { enableMapClick: true })
- } catch (e) {
- console.error('创建地图实例失败:', e)
- mapError.value = '百度地图初始化失败,请检查AK和网络连接。'
- return
- }
- try {
- const [clng, clat] = convertCoord(centerPoint.lng, centerPoint.lat)
- const centerBMapPoint = new BMapGL.Point(clng, clat)
- mapInstance.centerAndZoom(centerBMapPoint, MAP_DEFAULT_ZOOM)
- mapInstance.enableScrollWheelZoom(true)
- } catch (e) {
- console.error('设置地图中心失败:', e)
- }
- try {
- if (typeof mapInstance.setMinZoom === 'function') {
- mapInstance.setMinZoom(MAP_MIN_ZOOM)
- }
- if (typeof mapInstance.setMaxZoom === 'function') {
- mapInstance.setMaxZoom(MAP_MAX_ZOOM)
- }
- } catch (e) {
- console.warn('设置缩放限制失败:', e)
- }
- try {
- if (typeof mapInstance.setMaxBounds === 'function') {
- const sw = new BMapGL.Point(MAP_BOUNDS.southWest.lng, MAP_BOUNDS.southWest.lat)
- const ne = new BMapGL.Point(MAP_BOUNDS.northEast.lng, MAP_BOUNDS.northEast.lat)
- mapInstance.setMaxBounds(new BMapGL.Bounds(sw, ne))
- }
- } catch (e) {
- console.warn('设置地图范围限制失败:', e)
- }
- mapInstance.addEventListener('click', () => {
- if (overlayClicked) {
- overlayClicked = false
- return
- }
- closeInfoWindow()
- selectedFeature.value = null
- })
- mapInstance.addEventListener('mousemove', (event) => {
- try {
- if (event && event.latlng) {
- currentCoordinate.value = `${event.latlng.lng.toFixed(4)}, ${event.latlng.lat.toFixed(4)}`
- }
- } catch (_) { /* ignore mousemove errors */ }
- })
- try {
- renderMapScene()
- } catch (e) {
- console.error('渲染地图要素失败:', e)
- mapError.value = '地图要素渲染失败。'
- }
- }
- function renderMapScene() {
- if (!mapInstance || typeof BMapGL === 'undefined') return
- closeInfoWindow()
- try { mapInstance.clearOverlays() } catch (_) { /* ignore */ }
- try { drawPipelines() } catch (e) { console.warn('绘制管线失败:', e) }
- try { drawPipePoints() } catch (e) { console.warn('绘制管点失败:', e) }
- try { drawManholes() } catch (e) { console.warn('绘制窨井失败:', e) }
- }
- function drawBoundary() {
- const pts = boundaryPoints.map(item => { const [lng, lat] = convertCoord(item.lng, item.lat); return new BMapGL.Point(lng, lat) })
- const polygon = new BMapGL.Polygon(pts, {
- strokeColor: '#52b788',
- strokeWeight: 2.5,
- strokeOpacity: 0.85,
- strokeStyle: 'dashed',
- fillColor: '#d8f3dc',
- fillOpacity: 0.22
- })
- mapInstance.addOverlay(polygon)
- }
- function drawPipelines() {
- pipelineData.forEach(item => {
- try {
- if (!layerState.pipelines[item.level]) return
- const pts = item.points.map(p => { const [lng, lat] = convertCoord(p.lng, p.lat); return new BMapGL.Point(lng, lat) })
- const polyline = new BMapGL.Polyline(pts, {
- strokeColor: colorMap[item.level],
- strokeWeight: 6,
- strokeOpacity: 0.92
- })
- const firstPt = { lng: convertCoord(item.points[0].lng, item.points[0].lat)[0], lat: convertCoord(item.points[0].lng, item.points[0].lat)[1] }
- try {
- polyline.addEventListener('click', (e) => {
- overlayClicked = true
- e.domEvent && e.domEvent.stopPropagation && e.domEvent.stopPropagation()
- showPopup(firstPt.lng, firstPt.lat, buildPopupHTML(buildPipelineDetail(item)))
- selectedFeature.value = buildPipelineDetail(item)
- })
- } catch (_) { /* polyline click listener failed */ }
- mapInstance.addOverlay(polyline)
- const rawMid = getMidPoint(item.points)
- const mid = { lng: convertCoord(rawMid.lng, rawMid.lat)[0], lat: convertCoord(rawMid.lng, rawMid.lat)[1] }
- addPipeLabel(mid.lng, mid.lat, item.name, colorMap[item.level], item)
- } catch (e) {
- console.warn(`绘制管线 ${item.name} 失败:`, e)
- }
- })
- }
- function drawPipePoints() {
- pipePointData.value.forEach(item => {
- try {
- if (!layerState.pipePoints[item.type]) return
- const [plng, plat] = convertCoord(item.lng, item.lat)
- addPointMarker(plng, plat, colorMap[item.type], item.name, 'dot', () => {
- showPopup(plng, plat, buildPopupHTML(buildPipePointDetail(item)))
- selectedFeature.value = buildPipePointDetail(item)
- })
- } catch (e) {
- console.warn(`绘制管点 ${item.name} 失败:`, e)
- }
- })
- }
- function drawManholes() {
- manholeData.value.forEach(item => {
- try {
- if (!layerState.manholes[item.type]) return
- const [mlng, mlat] = convertCoord(item.lng, item.lat)
- addPointMarker(mlng, mlat, colorMap[item.type], item.name, 'square', () => {
- showPopup(mlng, mlat, buildPopupHTML(buildManholeDetail(item)))
- selectedFeature.value = buildManholeDetail(item)
- })
- } catch (e) {
- console.warn(`绘制窨井 ${item.name} 失败:`, e)
- }
- })
- }
- function addPipeLabel(lng, lat, text, color, rawItem) {
- const point = new BMapGL.Point(lng, lat)
- const html = `<div class="pipe-label" style="color:${color}">${text}</div>`
- const label = new BMapGL.Label(html, {
- position: point,
- offset: new BMapGL.Size(-28, -8)
- })
- label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
- try {
- label.addEventListener('click', (e) => {
- overlayClicked = true
- e.domEvent && e.domEvent.stopPropagation && e.domEvent.stopPropagation()
- showPopup(lng, lat, buildPopupHTML(buildPipelineDetail(rawItem)))
- selectedFeature.value = buildPipelineDetail(rawItem)
- })
- } catch (_) { /* label click listener failed */ }
- mapInstance.addOverlay(label)
- }
- function addPointMarker(lng, lat, color, name, shape, onClick) {
- const point = new BMapGL.Point(lng, lat)
- const size = 22
- const borderRadius = shape === 'square' ? '3px' : '50%'
- const html = `<div class="marker-wrap" style="cursor:pointer">
- <div style="width:${size}px;height:${size}px;background:${color};border-radius:${borderRadius};border:3px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,0.25);margin:0 auto;"></div>
- <div class="marker-text">${name}</div>
- </div>`
- const label = new BMapGL.Label(html, {
- position: point,
- offset: new BMapGL.Size(-size / 2 - 3, -size - 8)
- })
- label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
- try {
- label.addEventListener('click', (e) => {
- overlayClicked = true
- e.domEvent && e.domEvent.stopPropagation && e.domEvent.stopPropagation()
- onClick && onClick()
- })
- } catch (_) { /* marker click listener failed */ }
- mapInstance.addOverlay(label)
- }
- function showPopup(lng, lat, html) {
- try {
- closeInfoWindow()
- const point = new BMapGL.Point(lng, lat)
- const infoWindow = new BMapGL.InfoWindow(html, {
- width: 310,
- title: '',
- enableMessage: false
- })
- activeInfoWindow = infoWindow
- mapInstance.openInfoWindow(infoWindow, point)
- } catch (e) {
- console.warn('弹出信息窗口失败:', e)
- }
- }
- function closeInfoWindow() {
- if (activeInfoWindow && mapInstance) {
- mapInstance.closeInfoWindow()
- activeInfoWindow = null
- }
- }
- function buildPopupHTML(detail) {
- const rows = Object.entries(detail.details)
- .map(([k, v]) => `<div class="popup-row"><span class="popup-label">${k}</span><span class="popup-value">${v}</span></div>`)
- .join('')
- return `<div class="info-popup">
- <div class="popup-header">${detail.name}</div>
- <div class="popup-tag">${detail.category}</div>
- <div class="popup-divider"></div>
- ${rows}
- </div>`
- }
- function buildPipelineDetail(item) {
- return {
- name: item.name,
- category: labelMap[item.level],
- details: {
- '管线编号': item.code,
- '所属道路': item.road,
- '管线长度': `${item.length} km`,
- '管径': item.diameter,
- '材质': item.material,
- '压力等级': item.pressure,
- '敷设方式': item.layingMethod,
- '埋深': item.depth,
- '投产日期': item.commissioningDate,
- '运行状态': item.status
- }
- }
- }
- function getPipelineName(pipelineId) {
- const found = pipelineData.find(p => p.id === pipelineId)
- return found ? found.name : '—'
- }
- function buildPipePointDetail(item) {
- return {
- name: item.name,
- category: labelMap[item.type],
- details: {
- '设备编号': item.code,
- '所属管线': getPipelineName(item.pipelineId),
- '所属道路': item.road,
- '设备型号': item.deviceModel,
- '口径': item.caliber,
- '运行状态': item.status,
- '经度': item.lng.toFixed(5),
- '纬度': item.lat.toFixed(5)
- }
- }
- }
- function buildManholeDetail(item) {
- return {
- name: item.name,
- category: labelMap[item.type],
- details: {
- '井盖编号': item.code,
- '所属管线': getPipelineName(item.pipelineId),
- '所属道路': item.road,
- '材质': item.material,
- '规格': item.size,
- '巡检状态': item.status,
- '经度': item.lng.toFixed(5),
- '纬度': item.lat.toFixed(5)
- }
- }
- }
- function getMidPoint(points) {
- return points[Math.floor(points.length / 2)]
- }
- function handleSearch() {
- const keyword = searchKeyword.value.trim()
- if (!keyword) {
- ElMessage.warning('请输入搜索关键字')
- return
- }
- const matched = searchableFeatures.value.find(item =>
- [item.name, item.code, item.road].some(text => text?.includes(keyword))
- )
- if (!matched) {
- ElMessage.info('未找到匹配要素')
- return
- }
- const [mlng, mlat] = convertCoord(matched.lng, matched.lat)
- mapInstance.centerAndZoom(new BMapGL.Point(mlng, mlat), 18)
- if (matched.kind === 'pipeline') {
- selectedFeature.value = buildPipelineDetail(matched.raw)
- showPopup(mlng, mlat, buildPopupHTML(selectedFeature.value))
- } else if (matched.kind === 'pipePoint') {
- selectedFeature.value = buildPipePointDetail(matched.raw)
- showPopup(mlng, mlat, buildPopupHTML(selectedFeature.value))
- } else {
- selectedFeature.value = buildManholeDetail(matched.raw)
- showPopup(mlng, mlat, buildPopupHTML(selectedFeature.value))
- }
- ElMessage.success(`已定位到 ${matched.name}`)
- }
- async function refreshScene() {
- layerState.pipelines = { high: true, subHigh: true, medium: true, low: true }
- layerState.pipePoints = { valve: true, regulator: true, flowmeter: true, terminal: true }
- layerState.manholes = { cover: true, inspection: true }
- searchKeyword.value = ''
- selectedFeature.value = null
- closeInfoWindow()
- await loadGisData()
- locateCenter()
- }
- function locateCenter() {
- if (!mapInstance) return
- const [clng, clat] = convertCoord(centerPoint.lng, centerPoint.lat)
- mapInstance.centerAndZoom(new BMapGL.Point(clng, clat), MAP_DEFAULT_ZOOM)
- }
- function zoomIn() {
- if (!mapInstance) return
- const z = mapInstance.getZoom()
- if (z < MAP_MAX_ZOOM) mapInstance.zoomIn()
- else ElMessage.info('已放大至最大级别')
- }
- function zoomOut() {
- if (!mapInstance) return
- const z = mapInstance.getZoom()
- if (z > MAP_MIN_ZOOM) mapInstance.zoomOut()
- else ElMessage.info('已缩小至最小级别')
- }
- function toggleFullscreen() {
- const target = screenRef.value
- if (!target) return
- if (!document.fullscreenElement) {
- target.requestFullscreen?.()
- return
- }
- document.exitFullscreen?.()
- }
- function handleFullscreenChange() {
- if (!mapInstance) return
- const [clng, clat] = convertCoord(centerPoint.lng, centerPoint.lat)
- setTimeout(() => {
- mapInstance.centerAndZoom(new BMapGL.Point(clng, clat), mapInstance.getZoom())
- }, 200)
- }
- </script>
- <style scoped>
- .gis-dashboard {
- height: calc(100vh - 84px);
- padding: 16px;
- background: #f2f5f9;
- display: flex;
- flex-direction: column;
- gap: 0;
- }
- .dashboard-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- padding: 10px 16px;
- margin-bottom: 14px;
- background: #fff;
- border-radius: 10px;
- box-shadow: 0 4px 16px rgba(15, 35, 95, 0.05);
- flex-shrink: 0;
- }
- .header-title {
- display: flex;
- align-items: center;
- gap: 20px;
- flex-wrap: wrap;
- }
- .title-main {
- display: flex;
- align-items: center;
- gap: 8px;
- color: #1f2d3d;
- font-size: 17px;
- font-weight: 700;
- }
- .title-metrics {
- display: flex;
- align-items: center;
- gap: 16px;
- flex-wrap: wrap;
- }
- .metric {
- position: relative;
- padding-left: 14px;
- color: #44546a;
- font-size: 13px;
- }
- .metric::before {
- content: '';
- position: absolute;
- left: 0;
- top: 50%;
- width: 9px;
- height: 9px;
- border-radius: 50%;
- transform: translateY(-50%);
- }
- .metric-danger::before { background: #e63946; }
- .metric-primary::before { background: #457b9d; }
- .metric-warning::before { background: #7f4f24; }
- .header-actions {
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .search-input {
- width: 300px;
- }
- .dashboard-content {
- flex: 1;
- display: flex;
- gap: 14px;
- min-height: 0;
- }
- .dashboard-sidebar {
- width: 320px;
- display: flex;
- flex-direction: column;
- gap: 14px;
- flex-shrink: 0;
- overflow-y: auto;
- }
- .panel-card {
- border: none;
- border-radius: 14px;
- box-shadow: 0 4px 18px rgba(15, 35, 95, 0.05);
- flex-shrink: 0;
- }
- .layer-group {
- display: flex;
- flex-direction: column;
- gap: 9px;
- padding-bottom: 16px;
- margin-bottom: 16px;
- border-bottom: 1px solid #eef2f8;
- }
- .layer-group-last {
- padding-bottom: 0;
- margin-bottom: 0;
- border-bottom: none;
- }
- .layer-title {
- color: #303133;
- font-size: 14px;
- font-weight: 600;
- }
- .legend-list {
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .legend-item {
- display: flex;
- align-items: center;
- gap: 10px;
- color: #4e5969;
- font-size: 13px;
- }
- .legend-line {
- width: 34px;
- height: 0;
- border-top-width: 3px;
- border-top-style: solid;
- border-radius: 999px;
- flex-shrink: 0;
- }
- .legend-high { border-top-color: #e63946; }
- .legend-sub-high { border-top-color: #f77f00; }
- .legend-medium { border-top-color: #fcbf49; }
- .legend-low { border-top-color: #2a9d8f; }
- .legend-dot,
- .legend-square {
- width: 12px;
- height: 12px;
- flex-shrink: 0;
- }
- .legend-dot { border-radius: 50%; }
- .legend-square { border-radius: 2px; }
- .legend-valve { background: #457b9d; }
- .legend-regulator { background: #e76f51; }
- .legend-flowmeter { background: #2d6a4f; }
- .legend-terminal { background: #6c757d; }
- .legend-cover { background: #7f4f24; }
- .legend-inspection { background: #b08968; }
- .stats-card {
- background: linear-gradient(180deg, #f8fbff 0%, #f3f7fb 100%);
- }
- .stats-list {
- display: flex;
- flex-direction: column;
- gap: 11px;
- }
- .stats-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- color: #4e5969;
- font-size: 13px;
- }
- .stats-row strong {
- color: #1f2d3d;
- font-size: 14px;
- }
- .detail-card {
- flex: 1;
- min-height: 280px;
- }
- .detail-body {
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .detail-name {
- color: #1f2d3d;
- font-size: 16px;
- font-weight: 700;
- }
- .detail-tag {
- display: inline-flex;
- align-items: center;
- width: fit-content;
- padding: 3px 10px;
- color: #2d6cdf;
- background: #edf4ff;
- border-radius: 999px;
- font-size: 12px;
- }
- .dashboard-map-panel {
- position: relative;
- flex: 1;
- display: flex;
- flex-direction: column;
- background: #fff;
- border-radius: 14px;
- overflow: hidden;
- box-shadow: 0 4px 18px rgba(15, 35, 95, 0.05);
- min-width: 0;
- }
- .map-toolbar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- padding: 10px 16px;
- border-bottom: 1px solid #edf2f7;
- flex-shrink: 0;
- }
- .toolbar-left {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .toolbar-right {
- color: #7a8699;
- font-size: 13px;
- }
- .gis-map {
- flex: 1;
- min-height: 0;
- background: #e9f0e5;
- }
- .map-error {
- position: absolute;
- inset: 52px 0 0;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #909399;
- background: rgba(255, 255, 255, 0.85);
- font-size: 15px;
- z-index: 10;
- }
- </style>
- <style>
- .pipe-label {
- padding: 2px 8px;
- font-size: 12px;
- font-weight: 600;
- line-height: 1.3;
- white-space: nowrap;
- text-shadow: 0 0 4px #fff, 0 0 4px #fff;
- background: rgba(255, 255, 255, 0.7);
- border-radius: 10px;
- }
- .marker-wrap {
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- .marker-text {
- margin-top: 3px;
- padding: 1px 7px;
- color: #2f3a4f;
- font-size: 11px;
- line-height: 1.3;
- white-space: nowrap;
- background: rgba(255, 255, 255, 0.78);
- border-radius: 10px;
- text-shadow: 0 0 3px #fff;
- }
- .info-popup {
- padding: 4px 2px;
- font-size: 13px;
- color: #333;
- }
- .popup-header {
- font-size: 15px;
- font-weight: 700;
- color: #1a1a2e;
- margin-bottom: 6px;
- }
- .popup-tag {
- display: inline-block;
- padding: 2px 10px;
- font-size: 11px;
- color: #1d4ed8;
- background: #eff6ff;
- border-radius: 999px;
- margin-bottom: 10px;
- }
- .popup-divider {
- height: 1px;
- background: #e5e7eb;
- margin-bottom: 10px;
- }
- .popup-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 5px 0;
- border-bottom: 1px dashed #f0f0f0;
- font-size: 12px;
- line-height: 1.5;
- }
- .popup-row:last-child {
- border-bottom: none;
- }
- .popup-label {
- color: #6b7280;
- flex-shrink: 0;
- margin-right: 12px;
- }
- .popup-value {
- color: #1f2937;
- font-weight: 500;
- text-align: right;
- word-break: break-all;
- }
- .BMap_cpyCtrl,
- .anchorBL {
- display: none;
- }
- </style>
|