<!--漏失监测-->
<!--漏失监测-->
<template>
  <div class="flow-monitor-dashboard">
    <!-- 页面头部 -->
    <div class="page-header">
      <div class="header-left">
        <el-icon><Connection /></el-icon>
        <span class="title">漏失计监测数据管理</span>
        <div class="stats-badge">
          <span class="stat-item"><i class="dot"></i>监测点位 {{ totalPoints }}</span>
          <span class="stat-item online"><i class="dot"></i>在线 {{ onlineCount }}</span>
          <span class="stat-item alert"><i class="dot"></i>异常 {{ alertCount }}</span>
        </div>
      </div>
      <div class="header-right">
        <el-button type="primary" :icon="Plus" @click="addPoint">新增点位</el-button>
        <el-button :icon="Download" @click="exportData">批量导出</el-button>
        <el-button :icon="Refresh" @click="refreshData">刷新</el-button>
      </div>
    </div>

    <!-- 多重查询区域 -->
    <div class="query-bar">
      <div class="query-row">
        <el-input
            v-model="queryParams.pointName"
            placeholder="点位名称"
            clearable
            style="width: 180px"
            size="default"
        />
        <el-input
            v-model="queryParams.pointCode"
            placeholder="点位编号"
            clearable
            style="width: 150px"
            size="default"
        />
        <el-select v-model="queryParams.status" placeholder="运行状态" clearable style="width: 120px" size="default">
          <el-option label="在线" value="online" />
          <el-option label="离线" value="offline" />
          <el-option label="异常" value="alert" />
        </el-select>
        <el-select v-model="queryParams.area" placeholder="所属区域" clearable style="width: 140px" size="default">
          <el-option label="浦东新区" value="浦东新区" />
          <el-option label="徐汇区" value="徐汇区" />
          <el-option label="黄浦区" value="黄浦区" />
        </el-select>
        <el-date-picker
            v-model="queryParams.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            style="width: 260px"
            size="default"
        />
        <el-button type="primary" :icon="Search" @click="searchPoints">查询</el-button>
        <el-button @click="resetQuery">重置</el-button>
      </div>
    </div>

    <!-- 统计卡片 -->
    <div class="stats-cards">
      <div class="stat-card">
        <div class="stat-value">22<span class="unit">万m³/h</span></div>
        <div class="stat-label">总瞬时漏失</div>
      </div>
      <div class="stat-card">
        <div class="stat-value">20<span class="unit">m³/h</span></div>
        <div class="stat-label">平均漏失</div>
      </div>
      <div class="stat-card">
        <div class="stat-value">{{ maxFlow }}<span class="unit">m³/h</span></div>
        <div class="stat-label">最大漏失</div>
        <div class="stat-trend">点位: {{ maxFlowPoint }}</div>
      </div>
      <div class="stat-card">
        <div class="stat-value">{{ onlineRate }}<span class="unit">%</span></div>
        <div class="stat-label">设备在线率</div>
      </div>
    </div>

    <!-- 主内容区：左侧点位列表 + 右侧GIS地图 -->
    <div class="main-layout">
      <!-- 左侧点位清单表格 -->
      <div class="point-table-panel">
        <div class="table-header">
          <span>漏失监测点位清单</span>
          <div class="table-actions">
            <el-button size="small" @click="exportSelected">导出选中</el-button>
            <el-button size="small" type="primary" @click="batchExport">批量导出全部</el-button>
          </div>
        </div>
        <el-table
            :data="paginatedPoints"
            stripe
            border
            style="width: 100%"
            @row-click="selectPointOnMap"
            @selection-change="handleSelectionChange"
            ref="tableRef"
        >
          <el-table-column type="selection" width="45" align="center" />
          <el-table-column type="index" label="序号" width="55" align="center" />
          <el-table-column prop="code" label="点位编号" width="120" sortable />
          <el-table-column prop="name" label="点位名称" min-width="150" show-overflow-tooltip />
          <el-table-column prop="area" label="所属区域" width="100" />
          <el-table-column prop="flow" label="瞬时漏失(m³/h)" width="120" sortable>
            <template #default="{ row }">
              <span :class="{ 'alert-value': row.flow > row.warningThreshold }">{{ row.flow }}</span>
            </template>
          </el-table-column>
          <el-table-column prop="cumulativeFlow" label="累计漏失(万m³)" width="120" sortable />
          <el-table-column prop="pressure" label="管压(kPa)" width="100" />
          <el-table-column prop="status" label="运行状态" width="90" align="center">
            <template #default="{ row }">
              <el-tag :type="row.status === 'online' ? 'success' : row.status === 'alert' ? 'danger' : 'info'" size="small">
                {{ row.status === 'online' ? '在线' : row.status === 'alert' ? '异常' : '离线' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="updateTime" label="最后通讯" width="150" sortable />
          <el-table-column label="操作" width="100" fixed="right" align="center">
            <template #default="{ row }">
              <el-button link type="primary" size="small" @click.stop="viewDetail(row)">详情</el-button>
              <el-button link type="success" size="small" @click.stop="locateOnMap(row)">定位</el-button>
            </template>
          </el-table-column>
        </el-table>
        <div class="pagination-wrapper">
          <el-pagination
              background
              layout="total, sizes, prev, pager, next"
              :total="filteredPoints.length"
              v-model:current-page="currentPage"
              v-model:page-size="pageSize"
              :page-sizes="[10, 20, 50]"
          />
        </div>
      </div>

      <!-- 右侧GIS地图 -->
      <div class="gis-panel">
        <div class="map-header">
          <span>点位分布地图</span>
          <div class="map-tools">
            <el-button-group>
              <el-button size="small" :icon="ZoomIn" @click="zoomIn">放大</el-button>
              <el-button size="small" :icon="ZoomOut" @click="zoomOut">缩小</el-button>
              <el-button size="small" :icon="Location" @click="fitBounds">全图</el-button>
            </el-button-group>
          </div>
        </div>
        <div class="map-container" ref="mapContainer">
          <canvas id="flowMapCanvas" ref="canvasRef" @click="onMapClick" @mousemove="onCanvasMouseMove"></canvas>
          <div v-if="hoverPoint" class="hover-card" :style="{ top: hoverY + 'px', left: hoverX + 'px' }">
            <div class="hover-title">{{ hoverPoint.name }}</div>
            <div>漏失: {{ hoverPoint.flow }} m³/h</div>
            <div>管压: {{ hoverPoint.pressure }} kPa</div>
            <div>状态: {{ hoverPoint.status === 'online' ? '在线' : hoverPoint.status === 'alert' ? '异常' : '离线' }}</div>
          </div>
        </div>
      </div>
    </div>

    <!-- 点位详情抽屉 -->
    <el-drawer v-model="detailDrawerVisible" :title="`点位详情 - ${currentPoint?.name}`" size="450px" direction="rtl">
      <div v-if="currentPoint" class="point-detail">
        <el-descriptions :column="1" border>
          <el-descriptions-item label="点位编号">{{ currentPoint.code }}</el-descriptions-item>
          <el-descriptions-item label="点位名称">{{ currentPoint.name }}</el-descriptions-item>
          <el-descriptions-item label="所属区域">{{ currentPoint.area }}</el-descriptions-item>
          <el-descriptions-item label="详细地址">{{ currentPoint.address }}</el-descriptions-item>
          <el-descriptions-item label="经纬度">{{ currentPoint.lng }}, {{ currentPoint.lat }}</el-descriptions-item>
          <el-descriptions-item label="瞬时漏失">{{ currentPoint.flow }} m³/h</el-descriptions-item>
          <el-descriptions-item label="累计漏失">{{ currentPoint.cumulativeFlow }} 万m³</el-descriptions-item>
          <el-descriptions-item label="管压">{{ currentPoint.pressure }} kPa</el-descriptions-item>
          <el-descriptions-item label="报警阈值">{{ currentPoint.warningThreshold }} m³/h</el-descriptions-item>
          <el-descriptions-item label="运行状态">
            <el-tag :type="currentPoint.status === 'online' ? 'success' : currentPoint.status === 'alert' ? 'danger' : 'info'">
              {{ currentPoint.status === 'online' ? '在线' : currentPoint.status === 'alert' ? '异常' : '离线' }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="设备型号">{{ currentPoint.deviceModel }}</el-descriptions-item>
          <el-descriptions-item label="安装日期">{{ currentPoint.installDate }}</el-descriptions-item>
          <el-descriptions-item label="最后通讯">{{ currentPoint.updateTime }}</el-descriptions-item>
        </el-descriptions>
        <div class="trend-section">
          <div class="sub-title">近24小时漏失趋势</div>
          <div class="trend-chart">
            <v-chart :option="trendChartOption" autoresize />
          </div>
        </div>
      </div>
    </el-drawer>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
import { Connection, Plus, Download, Refresh, Search, ZoomIn, ZoomOut, Location } from '@element-plus/icons-vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
import { ElMessage } from 'element-plus'

use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])

// 模拟漏失监测点数据
const flowPoints = ref([
  { id: 1, code: 'FM-001', name: '世纪大道漏失计', area: '浦东新区', address: '世纪大道福山路口', lng: 121.505, lat: 31.235,
    flow: 1250, cumulativeFlow: 328.5, pressure: 385, warningThreshold: 1200,
    status: 'online', deviceModel: 'FL-2000', installDate: '2024-03-15', updateTime: '2025-04-02 15:23:45',
    trendData: [1150, 1180, 1200, 1220, 1250, 1240, 1230, 1210, 1190, 1180, 1170, 1160, 1150, 1140, 1160, 1180, 1200, 1220, 1250, 1240, 1230, 1210, 1190, 1180] },
  { id: 2, code: 'FM-002', name: '张杨路漏失计', area: '浦东新区', address: '张杨路松林路口', lng: 121.52, lat: 31.242,
    flow: 980, cumulativeFlow: 256.3, pressure: 350, warningThreshold: 1000,
    status: 'online', deviceModel: 'FL-2000', installDate: '2024-05-20', updateTime: '2025-04-02 15:22:30',
    trendData: [900, 920, 940, 960, 980, 970, 960, 950, 940, 930, 920, 910, 900, 890, 910, 930, 950, 970, 980, 970, 960, 950, 940, 930] },
  { id: 3, code: 'FM-003', name: '浦东南路漏失计', area: '浦东新区', address: '浦东南路400号', lng: 121.49, lat: 31.23,
    flow: 2100, cumulativeFlow: 512.8, pressure: 420, warningThreshold: 1800,
    status: 'alert', deviceModel: 'FL-3000', installDate: '2024-01-10', updateTime: '2025-04-02 15:21:15',
    trendData: [1800, 1850, 1900, 1950, 2000, 2050, 2100, 2080, 2050, 2020, 1980, 1950, 1920, 1900, 1880, 1860, 1840, 1820, 1800, 1780, 1760, 1740, 1720, 1700] },
  { id: 4, code: 'FM-004', name: '龙阳路漏失计', area: '浦东新区', address: '龙阳路白杨路口', lng: 121.51, lat: 31.215,
    flow: 680, cumulativeFlow: 185.2, pressure: 310, warningThreshold: 800,
    status: 'online', deviceModel: 'FL-2000', installDate: '2024-07-01', updateTime: '2025-04-02 15:20:00',
    trendData: [620, 640, 660, 680, 670, 660, 650, 640, 630, 620, 610, 600, 590, 580, 600, 620, 640, 660, 680, 670, 660, 650, 640, 630] },
  { id: 5, code: 'FM-005', name: '东方路漏失计', area: '浦东新区', address: '东方路蓝村路口', lng: 121.53, lat: 31.248,
    flow: 1520, cumulativeFlow: 398.6, pressure: 370, warningThreshold: 1400,
    status: 'online', deviceModel: 'FL-2500', installDate: '2024-09-15', updateTime: '2025-04-02 15:19:30',
    trendData: [1400, 1430, 1460, 1490, 1520, 1510, 1500, 1480, 1460, 1440, 1420, 1400, 1380, 1360, 1380, 1400, 1420, 1440, 1460, 1480, 1500, 1480, 1460, 1440] },
  { id: 6, code: 'FM-006', name: '人民路漏失计', area: '黄浦区', address: '人民路与世纪大道路口', lng: 121.487, lat: 31.239,
    flow: 1850, cumulativeFlow: 468.2, pressure: 395, warningThreshold: 1600,
    status: 'alert', deviceModel: 'FL-3000', installDate: '2024-11-20', updateTime: '2025-04-02 15:18:45',
    trendData: [1600, 1650, 1700, 1750, 1800, 1850, 1830, 1800, 1780, 1750, 1720, 1700, 1680, 1660, 1640, 1620, 1600, 1580, 1560, 1540, 1520, 1500, 1480, 1460] },
  { id: 7, code: 'FM-007', name: '淮海路漏失计', area: '徐汇区', address: '淮海中路陕西南路', lng: 121.475, lat: 31.225,
    flow: 850, cumulativeFlow: 225.4, pressure: 340, warningThreshold: 900,
    status: 'offline', deviceModel: 'FL-2000', installDate: '2024-12-05', updateTime: '2025-04-02 08:00:00',
    trendData: [800, 820, 840, 850, 840, 830, 820, 810, 800, 790, 780, 770, 760, 750, 740, 730, 720, 710, 700, 690, 680, 670, 660, 650] }
])

// 统计数据
const totalPoints = computed(() => flowPoints.value.length)
const onlineCount = computed(() => flowPoints.value.filter(p => p.status === 'online').length)
const alertCount = computed(() => flowPoints.value.filter(p => p.status === 'alert').length)
const totalFlow = computed(() => flowPoints.value.reduce((sum, p) => sum + p.flow, 0) / 10000)
const avgFlow = computed(() => (flowPoints.value.reduce((sum, p) => sum + p.flow, 0) / totalPoints.value).toFixed(0))
const maxFlow = computed(() => Math.max(...flowPoints.value.map(p => p.flow)))
const maxFlowPoint = computed(() => flowPoints.value.find(p => p.flow === maxFlow.value)?.name || '—')
const onlineRate = computed(() => ((onlineCount.value / totalPoints.value) * 100).toFixed(1))

// 查询参数
const queryParams = ref({
  pointName: '',
  pointCode: '',
  status: '',
  area: '',
  dateRange: []
})
const currentPage = ref(1)
const pageSize = ref(10)
const selectedRows = ref([])
const tableRef = ref(null)

// 过滤后的点位
const filteredPoints = computed(() => {
  let list = flowPoints.value
  if (queryParams.value.pointName) {
    list = list.filter(p => p.name.includes(queryParams.value.pointName))
  }
  if (queryParams.value.pointCode) {
    list = list.filter(p => p.code.includes(queryParams.value.pointCode))
  }
  if (queryParams.value.status) {
    list = list.filter(p => p.status === queryParams.value.status)
  }
  if (queryParams.value.area) {
    list = list.filter(p => p.area === queryParams.value.area)
  }
  return list
})

const paginatedPoints = computed(() => {
  const start = (currentPage.value - 1) * pageSize.value
  return filteredPoints.value.slice(start, start + pageSize.value)
})

// GIS画布相关
const canvasRef = ref(null)
const mapContainer = ref(null)
let ctx = null
let mapWidth = 0, mapHeight = 0
let mapZoom = 1
let offsetX = 0, offsetY = 0
let dragStart = null
let isDragging = false
const bounds = { minLng: 121.45, maxLng: 121.56, minLat: 31.20, maxLat: 31.27 }

// 交互状态
const hoverPoint = ref(null)
const hoverX = ref(0), hoverY = ref(0)
const currentPoint = ref(null)
const detailDrawerVisible = ref(false)

function worldToCanvas(lng, lat) {
  const px = (lng - bounds.minLng) / (bounds.maxLng - bounds.minLng) * mapWidth
  const py = (1 - (lat - bounds.minLat) / (bounds.maxLat - bounds.minLat)) * mapHeight
  const cx = mapWidth / 2, cy = mapHeight / 2
  return { x: (px - cx) * mapZoom + cx + offsetX, y: (py - cy) * mapZoom + cy + offsetY }
}

function drawBaseMap() {
  if (!ctx) return
  ctx.fillStyle = '#eaf5e9'
  ctx.fillRect(0, 0, mapWidth, mapHeight)
  ctx.strokeStyle = '#c8ddc4'
  ctx.lineWidth = 1
  for (let i = 0; i <= 10; i++) {
    const x = (i / 10) * mapWidth
    ctx.beginPath()
    ctx.moveTo(x, 0); ctx.lineTo(x, mapHeight); ctx.stroke()
    const y = (i / 10) * mapHeight
    ctx.beginPath()
    ctx.moveTo(0, y); ctx.lineTo(mapWidth, y); ctx.stroke()
  }
  ctx.fillStyle = '#a8d0a6'
  ctx.font = '12px "Microsoft YaHei"'
  ctx.fillText('世纪大道', 350, 120)
  ctx.fillText('张杨路', 450, 280)
  ctx.fillText('东方路', 580, 200)
}

function drawPoints() {
  filteredPoints.value.forEach(point => {
    const { x, y } = worldToCanvas(point.lng, point.lat)
    if (x < -20 || x > mapWidth + 20 || y < -20 || y > mapHeight + 20) return

    let color = '#67c23a'
    let size = 10
    if (point.status === 'alert') { color = '#f56c6c'; size = 14 }
    else if (point.status === 'offline') { color = '#909399'; size = 8 }

    ctx.beginPath()
    ctx.arc(x, y, size, 0, 2 * Math.PI)
    ctx.fillStyle = color
    ctx.fill()
    ctx.strokeStyle = '#fff'
    ctx.lineWidth = 2
    ctx.stroke()

    if (point.status === 'alert') {
      ctx.beginPath()
      ctx.arc(x, y, size + 4, 0, 2 * Math.PI)
      ctx.fillStyle = 'rgba(245,108,108,0.3)'
      ctx.fill()
    }

    ctx.fillStyle = '#333'
    ctx.font = '10px sans-serif'
    ctx.fillText(point.name.slice(0, 4), x - 12, y - 8)
  })
}

function renderMap() {
  if (!canvasRef.value || !mapContainer.value) return
  const canvas = canvasRef.value
  mapWidth = mapContainer.value.clientWidth
  mapHeight = mapContainer.value.clientHeight
  canvas.width = mapWidth
  canvas.height = mapHeight
  ctx = canvas.getContext('2d')
  drawBaseMap()
  drawPoints()
}

function zoomIn() { mapZoom = Math.min(mapZoom + 0.15, 2.5); renderMap() }
function zoomOut() { mapZoom = Math.max(mapZoom - 0.15, 0.6); renderMap() }
function fitBounds() { mapZoom = 1; offsetX = 0; offsetY = 0; renderMap() }

function onMapClick(e) {
  const rect = canvasRef.value.getBoundingClientRect()
  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
  let minDist = 20, hit = null
  filteredPoints.value.forEach(point => {
    const { x, y } = worldToCanvas(point.lng, point.lat)
    const dist = Math.hypot(mouseX - x, mouseY - y)
    if (dist < minDist) { minDist = dist; hit = point }
  })
  if (hit) { currentPoint.value = hit; detailDrawerVisible.value = true }
}

function onCanvasMouseMove(e) {
  const rect = canvasRef.value.getBoundingClientRect()
  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
  let minDist = 20, hit = null
  filteredPoints.value.forEach(point => {
    const { x, y } = worldToCanvas(point.lng, point.lat)
    const dist = Math.hypot(mouseX - x, mouseY - y)
    if (dist < minDist) { minDist = dist; hit = point }
  })
  if (hit) {
    hoverPoint.value = hit
    hoverX.value = e.clientX + 12
    hoverY.value = e.clientY - 40
  } else {
    hoverPoint.value = null
  }
}

function initDragEvents() {
  const canvas = canvasRef.value
  if (!canvas) return
  canvas.addEventListener('mousedown', (e) => {
    dragStart = { x: e.clientX, y: e.clientY, offsetX: offsetX, offsetY: offsetY }
    isDragging = true
    canvas.style.cursor = 'grabbing'
  })
  window.addEventListener('mousemove', (e) => {
    if (!isDragging) return
    const dx = e.clientX - dragStart.x, dy = e.clientY - dragStart.y
    offsetX = dragStart.offsetX + dx; offsetY = dragStart.offsetY + dy
    renderMap()
  })
  window.addEventListener('mouseup', () => {
    isDragging = false
    if (canvasRef.value) canvasRef.value.style.cursor = 'crosshair'
  })
}

// 趋势图配置
const trendChartOption = computed(() => {
  if (!currentPoint.value) return {}
  const times = Array.from({ length: 24 }, (_, i) => `${i}:00`)
  const threshold = currentPoint.value.warningThreshold
  return {
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: times, axisLabel: { rotate: 45, interval: 4 } },
    yAxis: { type: 'value', name: '漏失 (m³/h)' },
    series: [
      { name: '瞬时漏失', type: 'line', data: currentPoint.value.trendData, smooth: true, lineStyle: { color: '#409eff', width: 2 }, areaStyle: { opacity: 0.1 } },
      { name: '报警阈值', type: 'line', data: Array(24).fill(threshold), lineStyle: { color: '#f56c6c', width: 2, type: 'dashed' }, symbol: 'none' }
    ]
  }
})

// 方法
const searchPoints = () => { currentPage.value = 1; renderMap() }
const resetQuery = () => {
  queryParams.value = { pointName: '', pointCode: '', status: '', area: '', dateRange: [] }
  currentPage.value = 1
  renderMap()
}
const refreshData = () => { renderMap(); ElMessage.success('数据已刷新') }
const addPoint = () => { ElMessage.info('新增点位（演示）') }
const exportData = () => { ElMessage.success('批量导出全部数据（演示）') }
const exportSelected = () => { ElMessage.success(`导出选中 ${selectedRows.value.length} 个点位（演示）`) }
const batchExport = () => { ElMessage.success('批量导出全部点位（演示）') }
const viewDetail = (row) => { currentPoint.value = row; detailDrawerVisible.value = true }
const selectPointOnMap = (row) => { locateOnMap(row); viewDetail(row) }
const locateOnMap = (row) => {
  const center = worldToCanvas(row.lng, row.lat)
  mapZoom = 2
  offsetX = mapWidth/2 - center.x
  offsetY = mapHeight/2 - center.y
  renderMap()
}
const handleSelectionChange = (val) => { selectedRows.value = val }

watch(() => filteredPoints.value, () => { currentPage.value = 1; renderMap() })

onMounted(() => {
  nextTick(() => { renderMap(); initDragEvents(); window.addEventListener('resize', renderMap) })
})
onUnmounted(() => window.removeEventListener('resize', renderMap))
</script>

<style scoped>
.flow-monitor-dashboard {
  background: #f0f2f6;
  min-height: 100vh;
  padding: 20px;
}
.page-header {
  background: white;
  border-radius: 16px;
  padding: 16px 24px;
  margin-bottom: 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.header-left { display: flex; align-items: center; gap: 20px; }
.header-left .title { font-size: 1.3rem; font-weight: 600; }
.stats-badge { display: flex; gap: 20px; }
.stat-item { font-size: 13px; display: flex; align-items: center; gap: 6px; }
.stat-item.online { color: #67c23a; }
.stat-item.alert { color: #f56c6c; }
.stat-item .dot { width: 8px; height: 8px; border-radius: 50%; background: #909399; }
.stat-item.online .dot { background: #67c23a; }
.stat-item.alert .dot { background: #f56c6c; }
.header-right { display: flex; gap: 12px; }

.query-bar { background: white; border-radius: 12px; padding: 16px; margin-bottom: 20px; }
.query-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }

.stats-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 20px; }
.stat-card { background: white; border-radius: 12px; padding: 16px; text-align: center; }
.stat-value { font-size: 28px; font-weight: bold; }
.stat-value .unit { font-size: 14px; font-weight: normal; }
.stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
.stat-trend { font-size: 11px; color: #606266; margin-top: 4px; }

.main-layout { display: flex; gap: 20px; }
.point-table-panel { flex: 1.5; background: white; border-radius: 16px; padding: 16px; display: flex; flex-direction: column; overflow: hidden; }
.table-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; font-weight: 600; }
.table-actions { display: flex; gap: 8px; }
.pagination-wrapper { margin-top: 16px; display: flex; justify-content: flex-end; }

.gis-panel { flex: 1; background: white; border-radius: 16px; display: flex; flex-direction: column; overflow: hidden; }
.map-header { padding: 12px 16px; border-bottom: 1px solid #e9ecef; display: flex; justify-content: space-between; align-items: center; font-weight: 600; }
.map-container { flex: 1; position: relative; cursor: crosshair; min-height: 500px; }
canvas { width: 100%; height: 100%; display: block; }
.hover-card {
  position: fixed;
  background: rgba(0,0,0,0.8);
  color: white;
  padding: 8px 12px;
  border-radius: 10px;
  font-size: 12px;
  pointer-events: none;
  z-index: 200;
}
.hover-title { font-weight: 600; margin-bottom: 4px; }
.point-detail { padding: 8px; }
.alert-value { color: #f56c6c; font-weight: bold; }
.sub-title { font-weight: 600; margin: 16px 0 12px 0; }
.trend-chart { height: 200px; }
</style>