| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878 |
- <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-source">水源地 {{ summaryStats.sourceCount }}处</span>
- <span class="metric metric-plant">水厂 {{ summaryStats.plantCount }}座</span>
- <span class="metric metric-pumpStation">泵站 {{ summaryStats.pumpStationCount }}座</span>
- <span class="metric metric-pipe">供水管网 {{ summaryStats.pipeCount }}条</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-for="item in facilityTypes" :key="item.value" v-model="layerState[item.value]">{{ item.label }}</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" v-for="item in facilityTypes" :key="item.value"><span class="legend-dot" :style="{ background: item.color }"></span><span>{{ item.label }}</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.sourceCount }}</strong></div>
- <div class="stats-row"><span>水厂</span><strong>{{ summaryStats.plantCount }}</strong></div>
- <div class="stats-row"><span>泵站</span><strong>{{ summaryStats.pumpStationCount }}</strong></div>
- <div class="stats-row"><span>供水管网</span><strong>{{ summaryStats.pipeCount }}</strong></div>
- <div class="stats-row"><span>用水户</span><strong>{{ summaryStats.userCount }}</strong></div>
- </div>
- </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">
- <el-result icon="warning" title="供水设施图层加载失败" :sub-title="mapError">
- <template #extra><el-button type="primary" @click="loadGisData">重试</el-button></template>
- </el-result>
- </div>
- <div v-else-if="mapEmpty" class="map-error">
- <el-empty description="当前筛选下没有可显示的供水设施" />
- </div>
- </section>
- </div>
- <el-drawer
- v-model="detailOpen"
- :title="detailTitle"
- direction="rtl"
- class="detail-drawer"
- size="560px"
- append-to-body
- >
- <el-skeleton v-if="detailLoading" :rows="8" animated />
- <el-result v-else-if="detailError" icon="warning" title="设施详情加载失败" :sub-title="detailError">
- <template #extra><el-button type="primary" @click="loadFacilityDetail">重试</el-button></template>
- </el-result>
- <el-descriptions v-else-if="detailRecord" :column="1" border>
- <el-descriptions-item v-for="item in detailEntries" :key="item.label" :label="item.label">
- {{ item.value }}
- </el-descriptions-item>
- </el-descriptions>
- <el-empty v-else description="暂无设施详情" />
- </el-drawer>
- </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 { getWaterFacilityOverview, getWaterFacilityFeatures, getWaterFacilityDetail } from '@/api/pipeNetwork/waterSupply'
- import { normalizeGeoJson, unwrapResponse } from '@/utils/waterSupplyModel'
- import { getRequestErrorMessage } from '@/utils/waterFacilityQuery'
- import { buildFacilityMapModel, filterFacilityMapModel, findFacilityFeature } from '@/utils/waterFacilityGisModel'
- 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 = () => ({
- source: true,
- plant: true,
- pumpStation: true,
- pipe: true,
- user: true
- })
- const facilityTypes = [
- { label: '水源地', value: 'source', color: '#457b9d' },
- { label: '水厂', value: 'plant', color: '#2a9d8f' },
- { label: '泵站', value: 'pumpStation', color: '#e76f51' },
- { label: '管网', value: 'pipe', color: '#fcbf49' },
- { label: '用水户', value: 'user', color: '#7f4f24' }
- ]
- const colorMap = {
- source: '#457b9d',
- plant: '#2a9d8f',
- pumpStation: '#e76f51',
- pipe: '#fcbf49',
- user: '#7f4f24'
- }
- const labelMap = {
- source: '水源地',
- plant: '水厂',
- pumpStation: '泵站',
- pipe: '供水管网',
- user: '用水户'
- }
- const mapRef = ref(null)
- const screenRef = ref(null)
- const searchKeyword = ref('')
- const currentCoordinate = ref(`${centerPoint.lng}, ${centerPoint.lat}`)
- const mapError = ref('')
- const dataLoaded = ref(false)
- const layerState = reactive(defaultLayerState())
- const facilityOverview = ref([])
- const pipelineData = ref([])
- const pointData = ref([])
- const detailOpen = ref(false)
- const detailLoading = ref(false)
- const detailError = ref('')
- const detailRecord = ref(null)
- const detailTarget = ref(null)
- let mapInstance = null
- let overlayClicked = false
- const typeCount = type => {
- const row = facilityOverview.value.find(item => item.facilityType === type)
- return Number(row?.totalCount ?? row?.count ?? 0)
- }
- const summaryStats = computed(() => ({
- sourceCount: typeCount('source'),
- plantCount: typeCount('plant'),
- pumpStationCount: typeCount('pumpStation'),
- pipeCount: typeCount('pipe'),
- userCount: typeCount('user')
- }))
- const fullMapModel = computed(() => ({ points: pointData.value, lines: pipelineData.value }))
- const visibleMapModel = computed(() => filterFacilityMapModel(fullMapModel.value, layerState))
- const mapEmpty = computed(() => dataLoaded.value && !mapError.value && visibleMapModel.value.isEmpty)
- const detailTitle = computed(() => `${labelMap[detailTarget.value?.type] || '供水设施'}详情`)
- const detailEntries = computed(() => Object.entries(detailRecord.value || {})
- .filter(([, value]) => value !== null && value !== undefined && typeof value !== 'object')
- .map(([label, value]) => ({ label, value: value === '' ? '-' : value })))
- 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
- }
- detailOpen.value = false
- })
- 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
- try { mapInstance.clearOverlays() } catch (_) { /* ignore */ }
- try { drawPipelines() } catch (e) { console.warn('绘制供水管网失败:', e) }
- try { drawPoints() } catch (e) { console.warn('绘制供水设施失败:', e) }
- }
- function drawPipelines() {
- pipelineData.value.forEach(item => {
- try {
- if (!layerState[item.type]) 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.type],
- strokeWeight: 6,
- strokeOpacity: 0.92
- })
- try {
- polyline.addEventListener('click', (e) => {
- overlayClicked = true
- e.domEvent && e.domEvent.stopPropagation && e.domEvent.stopPropagation()
- openFacilityDetail(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.type], item)
- } catch (e) {
- console.warn(`绘制供水管网 ${item.name} 失败:`, e)
- }
- })
- }
- function drawPoints() {
- pointData.value.forEach(item => {
- try {
- if (!layerState[item.type]) return
- const [plng, plat] = convertCoord(item.lng, item.lat)
- addPointMarker(plng, plat, colorMap[item.type], item.name, 'dot', () => {
- openFacilityDetail(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()
- openFacilityDetail(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 getMidPoint(points) {
- const first = points[0]
- const last = points[points.length - 1]
- return {
- lng: (first.lng + last.lng) / 2,
- lat: (first.lat + last.lat) / 2
- }
- }
- async function loadGisData() {
- mapError.value = ''
- dataLoaded.value = false
- try {
- const [overviewRes, featureRes] = await Promise.all([
- getWaterFacilityOverview(),
- getWaterFacilityFeatures()
- ])
- const overviewData = unwrapResponse(overviewRes)
- facilityOverview.value = Array.isArray(overviewData) ? overviewData : (overviewData?.facilityOverview || [])
- const geoJson = normalizeGeoJson(featureRes)
- const model = buildFacilityMapModel(geoJson)
- pipelineData.value = model.lines
- pointData.value = model.points
- dataLoaded.value = true
- if (mapInstance && typeof BMapGL !== 'undefined') renderMapScene()
- } catch (e) {
- console.error('加载供水GIS数据失败', e)
- facilityOverview.value = []
- pipelineData.value = []
- pointData.value = []
- dataLoaded.value = true
- mapError.value = getRequestErrorMessage(e, '供水GIS数据加载失败,请重试。')
- }
- }
- function handleSearch() {
- const keyword = searchKeyword.value.trim()
- if (!keyword) {
- ElMessage.warning('请输入搜索关键字')
- return
- }
- const matched = findFacilityFeature(visibleMapModel.value, keyword)
- if (!matched) {
- ElMessage.info('未找到匹配要素')
- return
- }
- const [mlng, mlat] = convertCoord(matched.lng, matched.lat)
- if (mapInstance && typeof BMapGL !== 'undefined') {
- mapInstance.centerAndZoom(new BMapGL.Point(mlng, mlat), 18)
- }
- openFacilityDetail(matched)
- ElMessage.success(`已定位到 ${matched.name}`)
- }
- async function openFacilityDetail(feature) {
- detailTarget.value = feature
- detailOpen.value = true
- detailRecord.value = null
- detailError.value = ''
- await loadFacilityDetail()
- }
- async function loadFacilityDetail() {
- const feature = detailTarget.value
- if (!feature?.type || feature.id === null || feature.id === undefined) return
- detailLoading.value = true
- detailError.value = ''
- try {
- detailRecord.value = unwrapResponse(await getWaterFacilityDetail(feature.type, feature.id))
- } catch (error) {
- detailRecord.value = null
- detailError.value = getRequestErrorMessage(error, '设施详情查询失败,请重试。')
- } finally {
- detailLoading.value = false
- }
- }
- async function refreshScene() {
- layerState.source = true
- layerState.plant = true
- layerState.pumpStation = true
- layerState.pipe = true
- layerState.user = true
- searchKeyword.value = ''
- detailOpen.value = false
- 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;
- flex-wrap: wrap;
- gap: 16px;
- margin: 8px 0;
- align-items: center;
- margin-bottom: 8px;
- }
- .metric {
- display: inline-flex;
- align-items: center; /* 垂直居中 */
- padding: 12px 16px; /* 左右内边距 */
- height: auto;
- min-height: 48px;
- font-size: 16px;
- line-height: 1.5;
- border-radius: 8px;
- min-width: 100px;
- white-space: nowrap;
- background: rgba(255, 255, 255, 0.06);
- border: 1px solid rgba(255, 255, 255, 0.12);
- color: #ecf6ff;
- box-shadow: 0 1px 8px rgba(0, 0, 0, 0.25);
- transition: all 0.25s ease;
- cursor: default; /* 如果希望可点击,可改 pointer */
- backdrop-filter: blur(4px); /* 毛玻璃效果(可选,需浏览器支持) */
- }
- .metric::before {
- content: '';
- position: absolute;
- left: 0;
- top: 50%;
- width: 9px;
- height: 9px;
- border-radius: 50%;
- transform: translateY(-50%);
- }
- /* 语义颜色强化 */
- .metric-source {
- background: rgba(69, 123, 157, 0.8);
- border-color: rgba(69, 123, 157, 0.6);
- color: #F0F4F6;
- }
- .metric-plant {
- background: rgba(42, 157, 143, 0.8);
- border-color: rgba(42, 157, 143, 0.6);
- color: #F0F4F6;
- }
- .metric-pumpStation {
- background: rgba(231, 111, 81, 0.8);
- border-color: rgba(231, 111, 81, 0.6);
- color: #F0F4F6;
- }
- .metric-pipe {
- background: rgba(252, 191, 73, 0.8);
- border-color: rgba(252, 191, 73, 0.6);
- color: #F0F4F6;
- }
- .metric:hover {
- transform: translateY(-2px);
- background: rgba(0, 149, 253, 0.5);
- border-color: rgba(0, 149, 253, 0.5);
- box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
- }
- .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: 8px;
- color: #4e5969;
- font-size: 14px;
- }
- .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;
- }
- .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;
- }
- .BMap_cpyCtrl,
- .anchorBL {
- display: none;
- }
- </style>
|