Explorar el Código

Merge remote-tracking branch 'origin/master'

zengy hace 23 horas
padre
commit
91ba17316f

+ 9 - 0
src/api/drainage.js

@@ -272,6 +272,15 @@ export function publishWarning(data) {
   })
 }
 
+// 各设备最新一条水位/液位监测数据(排水设备运行监测页使用)
+export function getLatestWaterLevelByDevice() {
+  return request({
+    url: '/water/monitor/visualization/latest/device',
+    method: 'get',
+    headers: { 'X-Silent-Error': 'true' }
+  })
+}
+
 // 排水分区关联设备
 export function bindPartitionEquipment(data) {
   return request({ url: '/api/drainage/partition/bindEquipment', method: 'post', data })

+ 38 - 0
src/api/drainageRisk.js

@@ -0,0 +1,38 @@
+import request from '@/utils/request'
+
+/**
+ * 排水管网综合风险评估
+ *
+ * 后端固定取 pipe_network_base 中 network_type=1 的排水管网,
+ * 关联的设备、报警、预警、易积水点、工程设施同样只取排水口径,不含燃气与供水。
+ */
+
+// 分页查询综合风险列表
+export function getDrainageRiskPage(params) {
+  return request({ url: '/api/drainage/risk/page', method: 'get', params })
+}
+
+// 全量统计(不受分页影响)
+export function getDrainageRiskStatistics(params) {
+  return request({ url: '/api/drainage/risk/statistics', method: 'get', params })
+}
+
+// 筛选下拉项:管线类别、所属区域、风险级别
+export function getDrainageRiskFilterOptions() {
+  return request({ url: '/api/drainage/risk/filterOptions', method: 'get' })
+}
+
+// 风险等级分布图数据
+export function getDrainageRiskMap(params) {
+  return request({ url: '/api/drainage/risk/map', method: 'get', params })
+}
+
+// 单条管网综合风险评估详情
+export function getDrainageRiskDetail(networkId) {
+  return request({ url: `/api/drainage/risk/${networkId}/detail`, method: 'get' })
+}
+
+// 重新计算风险快照
+export function refreshDrainageRisk() {
+  return request({ url: '/api/drainage/risk/refresh', method: 'post' })
+}

+ 1 - 1
src/utils/drainageVideoModel.js

@@ -2,7 +2,7 @@ export const videoLayers = [
   { value: 'GB28181', label: '国标视频', color: '#409eff' },
   { value: 'MANUAL', label: '地址视频', color: '#67c23a' }
 ]
-export const districts = [{ value: 0, label: '城南' }, { value: 1, label: '城北' }, { value: 2, label: '太常' }]
+// 所属片区(0城南/1城北/2太常)已改为读字典 drainage_district,此处不再维护第二份写死数据
 export function hasVideoCoordinates(row) {
   if (row.longitude == null || row.latitude == null || row.longitude === '' || row.latitude === '') return false
   const lng = Number(row.longitude), lat = Number(row.latitude)

+ 460 - 0
src/views/subSystem/drainage/fxpg/components/RiskAssessmentTabs.vue

@@ -0,0 +1,460 @@
+<template>
+  <div class="risk-detail" v-if="detail">
+    <!-- 风险结论横幅 -->
+    <div class="risk-banner" :style="{ borderColor: riskColor, background: riskBg }">
+      <div class="banner-score">
+        <div class="score-value" :style="{ color: riskColor }">{{ assess.riskScore ?? '-' }}</div>
+        <div class="score-label">综合风险评分</div>
+      </div>
+      <el-progress type="dashboard" :percentage="assess.riskScore || 0" :width="96" :stroke-width="8"
+        :color="riskColor" class="banner-progress">
+        <template #default>
+          <span class="progress-text" :style="{ color: riskColor }">{{ assess.riskLevelName || '-' }}</span>
+        </template>
+      </el-progress>
+      <div class="banner-facts">
+        <div class="fact-item">
+          <span class="fact-value">{{ pipeName }}</span>
+          <span class="fact-label">管网名称</span>
+        </div>
+        <div class="fact-item">
+          <span class="fact-value">{{ assess.equipmentCount ?? 0 }}</span>
+          <span class="fact-label">关联设备</span>
+        </div>
+        <div class="fact-item">
+          <span class="fact-value" :class="{ danger: (assess.alarmCount || 0) > 0 }">{{ assess.alarmCount ?? 0 }}</span>
+          <span class="fact-label">近30天报警</span>
+        </div>
+        <div class="fact-item">
+          <span class="fact-value" :class="{ danger: (assess.waterloggingCount || 0) > 0 }">{{
+            assess.waterloggingCount ?? 0 }}</span>
+          <span class="fact-label">周边易积水点</span>
+        </div>
+      </div>
+    </div>
+
+    <el-tabs v-model="activeTab" class="risk-tabs">
+      <!-- 基本信息 -->
+      <el-tab-pane label="基本信息" name="basic">
+        <el-descriptions :column="2" border size="default">
+          <el-descriptions-item label="管线编号">{{ basic.networkCode || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="管网名称">{{ basic.networkName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="管线类别">{{ basic.networkLevel || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="所属区域">{{ basic.managementUnit || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="所在道路" :span="2">{{ basic.location || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="运行状态">{{ basic.status || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="评估时间">{{ formatDateTime(assess.assessmentTime) }}</el-descriptions-item>
+          <el-descriptions-item label="创建时间">{{ formatDateTime(basic.createTime) }}</el-descriptions-item>
+          <el-descriptions-item label="更新时间">{{ formatDateTime(basic.updateTime) }}</el-descriptions-item>
+          <el-descriptions-item label="评估模型" :span="2">{{ assess.assessmentModel || '-' }}</el-descriptions-item>
+        </el-descriptions>
+      </el-tab-pane>
+
+      <!-- 管线信息 -->
+      <el-tab-pane label="管线信息" name="pipe">
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="管线编号">{{ basic.networkCode || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="管线类别">{{ pipe.networkLevel || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="管材">{{ pipe.material || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="管径">{{ pipe.diameter ? `${pipe.diameter} mm` : '-' }}</el-descriptions-item>
+          <el-descriptions-item label="管段长度">{{ pipe.length ? `${pipe.length} m` : '-' }}</el-descriptions-item>
+          <el-descriptions-item label="建设日期">{{ formatDate(pipe.buildDate) }}</el-descriptions-item>
+          <el-descriptions-item label="管龄">{{ pipe.pipeAge ? `${pipe.pipeAge} 年` : '未登记' }}</el-descriptions-item>
+          <el-descriptions-item label="坐标完整性">
+            <el-tag :type="hasCoordinate ? 'success' : 'warning'" size="small">
+              {{ hasCoordinate ? '起终点坐标齐全' : '缺少起终点坐标' }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="起点坐标">{{ formatPoint(pipe.startLongitude, pipe.startLatitude) }}</el-descriptions-item>
+          <el-descriptions-item label="终点坐标">{{ formatPoint(pipe.endLongitude, pipe.endLatitude) }}</el-descriptions-item>
+        </el-descriptions>
+      </el-tab-pane>
+
+      <!-- 评估详情 -->
+      <el-tab-pane label="评估详情" name="assessment">
+        <div class="dimension-grid">
+          <div class="dimension-card" v-for="dim in dimensions" :key="dim.key">
+            <div class="dimension-head">
+              <span class="dimension-label">{{ dim.label }}</span>
+              <span class="dimension-score" :style="{ color: dimColor(dim) }">{{ dim.score }}<i>/{{ dim.max }}</i></span>
+            </div>
+            <el-progress :percentage="Math.round((dim.score / dim.max) * 100)" :show-text="false"
+              :stroke-width="6" :color="dimColor(dim)" />
+            <div class="dimension-items">
+              <div class="dimension-item" v-for="(item, i) in dim.items" :key="i">
+                <span class="item-label">{{ item.label }}</span>
+                <span class="item-detail">{{ item.detail }}</span>
+                <span class="item-score">+{{ item.score }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="assess-note">
+          评分口径:管网本体 35 分 + 运行报警 35 分 + 环境叠加 20 分 + 数据质量 10 分,合计 100 分;
+          ≥70 为高风险,≥40 为中风险,其余为低风险。
+        </div>
+      </el-tab-pane>
+
+      <!-- 评估分析 -->
+      <el-tab-pane label="评估分析" name="analysis">
+        <div class="analysis-block">
+          <div class="block-title">风险构成要素</div>
+          <ul class="factor-list">
+            <li v-for="(factor, i) in factors" :key="i">
+              <span class="factor-index">{{ i + 1 }}</span>
+              <span>{{ factor }}</span>
+            </li>
+          </ul>
+        </div>
+
+        <div class="analysis-block">
+          <div class="block-title">处置建议</div>
+          <ul class="measure-list">
+            <li v-for="(measure, i) in measures" :key="i">
+              <el-icon class="measure-icon"><Select /></el-icon>
+              <span>{{ measure }}</span>
+            </li>
+          </ul>
+        </div>
+
+        <el-descriptions :column="1" border class="analysis-desc">
+          <el-descriptions-item label="周边防护目标">{{ analysis.protectiveTargets || '无' }}</el-descriptions-item>
+          <el-descriptions-item label="周边危险源">{{ analysis.hazardSources || '无' }}</el-descriptions-item>
+        </el-descriptions>
+
+        <div class="analysis-block" v-if="equipments.length">
+          <div class="block-title">周边关联设备</div>
+          <el-table :data="equipments" border size="small">
+            <el-table-column prop="equipmentName" label="设备名称" min-width="160" show-overflow-tooltip />
+            <el-table-column prop="equipmentCode" label="设备编码" min-width="140" show-overflow-tooltip />
+            <el-table-column prop="equipmentLocation" label="安装位置" min-width="180" show-overflow-tooltip />
+          </el-table>
+        </div>
+
+        <div class="analysis-block">
+          <div class="block-title">最近报警记录</div>
+          <el-table :data="recentAlarms" border size="small" empty-text="近 30 天无报警记录">
+            <el-table-column label="报警时间" width="170">
+              <template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
+            </el-table-column>
+            <el-table-column prop="deviceCode" label="设备编码" min-width="140" show-overflow-tooltip />
+            <el-table-column label="等级" width="80" align="center">
+              <template #default="{ row }">
+                <el-tag v-if="row.alarmLevel" size="small" :type="row.alarmLevel <= 2 ? 'danger' : 'warning'">
+                  {{ row.alarmLevel }}级
+                </el-tag>
+                <span v-else>-</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="alarmContent" label="报警内容" min-width="200" show-overflow-tooltip />
+            <el-table-column label="状态" width="90" align="center">
+              <template #default="{ row }">
+                <el-tag size="small" :type="row.alarmStatus === 0 ? 'warning' : 'success'">
+                  {{ row.alarmStatus === 0 ? '未处置' : '已处置' }}
+                </el-tag>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script setup name="RiskAssessmentTabs">
+import { ref, computed } from 'vue'
+import { Select } from '@element-plus/icons-vue'
+
+const props = defineProps({
+  /** 后端 /api/drainage/risk/{id}/detail 返回的四块数据 */
+  detail: { type: Object, default: null }
+})
+
+const activeTab = ref('basic')
+
+const basic = computed(() => props.detail?.basic || {})
+const pipe = computed(() => props.detail?.pipeInfo || {})
+const assess = computed(() => props.detail?.assessment || {})
+const analysis = computed(() => props.detail?.analysis || {})
+
+const dimensions = computed(() => assess.value.dimensions || [])
+const factors = computed(() => analysis.value.riskFactors || [])
+const measures = computed(() => analysis.value.mitigationMeasures || [])
+const equipments = computed(() => assess.value.equipments || [])
+const recentAlarms = computed(() => analysis.value.recentAlarms || [])
+
+const pipeName = computed(() => basic.value.networkName || '-')
+const riskColor = computed(() => assess.value.riskColor || '#409EFF')
+const riskBg = computed(() => hexToRgba(riskColor.value, 0.06))
+
+const hasCoordinate = computed(() =>
+  pipe.value.startLongitude != null && pipe.value.startLatitude != null &&
+  pipe.value.endLongitude != null && pipe.value.endLatitude != null)
+
+/** 维度内条目得分越高越危险,配色跟着走 */
+function dimColor(dim) {
+  if (!dim || !dim.max) return '#409EFF'
+  const ratio = dim.score / dim.max
+  if (ratio >= 0.7) return '#F56C6C'
+  if (ratio >= 0.4) return '#E6A23C'
+  return '#67C23A'
+}
+
+function hexToRgba(hex, alpha) {
+  const value = String(hex).replace('#', '')
+  if (value.length !== 6) return 'transparent'
+  const r = parseInt(value.slice(0, 2), 16)
+  const g = parseInt(value.slice(2, 4), 16)
+  const b = parseInt(value.slice(4, 6), 16)
+  return `rgba(${r}, ${g}, ${b}, ${alpha})`
+}
+
+function formatDateTime(value) {
+  if (!value) return '-'
+  return String(value).replace('T', ' ').slice(0, 19)
+}
+
+function formatDate(value) {
+  if (!value) return '-'
+  return String(value).replace('T', ' ').slice(0, 10)
+}
+
+function formatPoint(lng, lat) {
+  if (lng == null || lat == null) return '-'
+  return `${lng}, ${lat}`
+}
+</script>
+
+<style scoped>
+.risk-detail {
+  padding: 0 4px;
+}
+
+/* ==================== 风险横幅 ==================== */
+.risk-banner {
+  display: flex;
+  align-items: center;
+  gap: 24px;
+  border: 1px solid #ebeef5;
+  border-left-width: 4px;
+  border-radius: 6px;
+  padding: 16px 20px;
+  margin-bottom: 16px;
+}
+
+.banner-score {
+  text-align: center;
+  flex-shrink: 0;
+}
+
+.score-value {
+  font-size: 38px;
+  font-weight: 700;
+  line-height: 1.1;
+  font-variant-numeric: tabular-nums;
+}
+
+.score-label {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
+
+.banner-progress {
+  flex-shrink: 0;
+}
+
+.progress-text {
+  font-size: 13px;
+  font-weight: 600;
+}
+
+.banner-facts {
+  flex: 1;
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 12px;
+  min-width: 0;
+}
+
+.fact-item {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  min-width: 0;
+}
+
+.fact-value {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.fact-value.danger {
+  color: #F56C6C;
+}
+
+.fact-label {
+  font-size: 12px;
+  color: #909399;
+}
+
+/* ==================== 维度卡片 ==================== */
+.dimension-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 14px;
+}
+
+.dimension-card {
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  padding: 14px 16px;
+  background: #fafbfc;
+}
+
+.dimension-head {
+  display: flex;
+  align-items: baseline;
+  justify-content: space-between;
+  margin-bottom: 8px;
+}
+
+.dimension-label {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.dimension-score {
+  font-size: 18px;
+  font-weight: 700;
+  font-variant-numeric: tabular-nums;
+}
+
+.dimension-score i {
+  font-style: normal;
+  font-size: 12px;
+  font-weight: 400;
+  color: #909399;
+  margin-left: 2px;
+}
+
+.dimension-items {
+  margin-top: 10px;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.dimension-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 12px;
+}
+
+.item-label {
+  color: #606266;
+  width: 92px;
+  flex-shrink: 0;
+}
+
+.item-detail {
+  flex: 1;
+  color: #909399;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.item-score {
+  color: #F56C6C;
+  font-weight: 600;
+  flex-shrink: 0;
+}
+
+.assess-note {
+  margin-top: 14px;
+  font-size: 12px;
+  color: #909399;
+  background: #f5f7fa;
+  border-radius: 4px;
+  padding: 10px 12px;
+  line-height: 1.7;
+}
+
+/* ==================== 评估分析 ==================== */
+.analysis-block {
+  margin-bottom: 18px;
+}
+
+.block-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 10px;
+  padding-left: 8px;
+  border-left: 3px solid #409EFF;
+  line-height: 1.2;
+}
+
+.factor-list,
+.measure-list {
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+
+.factor-list li {
+  display: flex;
+  align-items: flex-start;
+  gap: 8px;
+  font-size: 13px;
+  color: #606266;
+  line-height: 1.7;
+  padding: 6px 10px;
+  border-radius: 4px;
+}
+
+.factor-list li:nth-child(odd) {
+  background: #fafbfc;
+}
+
+.factor-index {
+  flex-shrink: 0;
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  background: rgba(245, 108, 108, 0.12);
+  color: #F56C6C;
+  font-size: 11px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-top: 2px;
+}
+
+.measure-list li {
+  display: flex;
+  align-items: flex-start;
+  gap: 8px;
+  font-size: 13px;
+  color: #606266;
+  line-height: 1.7;
+  padding: 5px 10px;
+}
+
+.measure-icon {
+  color: #67C23A;
+  margin-top: 4px;
+  flex-shrink: 0;
+}
+
+.analysis-desc {
+  margin-bottom: 18px;
+}
+</style>

+ 339 - 271
src/views/subSystem/drainage/fxpg/zhfxtjfx.vue

@@ -1,105 +1,160 @@
 <template>
-  <div class="app-container">
-    <!-- 搜索区域 -->
-    <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
-      <el-form-item label="管网名称">
-        <el-input v-model="queryParams.name" placeholder="请输入" clearable style="width: 160px" />
+  <div class="app-container risk-stat-page">
+    <!-- ==================== 筛选区 ==================== -->
+    <el-form :model="queryParams" :inline="true" v-show="showSearch" label-width="80px" @submit.prevent>
+      <el-form-item label="管线编号">
+        <el-input v-model="queryParams.networkCode" placeholder="请输入" clearable style="width: 150px"
+          @keyup.enter="handleQuery" />
       </el-form-item>
-      <el-form-item label="管网类型">
-        <el-select v-model="queryParams.networkType" placeholder="请选择" clearable style="width: 160px">
-          <el-option v-for="item in networkTypeOptions" :key="item" :label="item" :value="item" />
+      <el-form-item label="管线类别">
+        <el-select v-model="queryParams.networkLevel" placeholder="全部" clearable style="width: 140px">
+          <el-option v-for="item in options.networkLevels" :key="item" :label="item" :value="item" />
         </el-select>
       </el-form-item>
       <el-form-item label="所属区域">
-        <el-input v-model="queryParams.area" placeholder="请输入" clearable style="width: 160px" />
+        <el-select v-model="queryParams.managementUnit" placeholder="全部" clearable filterable style="width: 160px">
+          <el-option v-for="item in options.managementUnits" :key="item" :label="item" :value="item" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="所在道路">
+        <el-input v-model="queryParams.location" placeholder="请输入" clearable style="width: 160px"
+          @keyup.enter="handleQuery" />
+      </el-form-item>
+      <el-form-item label="风险级别">
+        <el-select v-model="queryParams.riskLevel" placeholder="全部" clearable style="width: 130px">
+          <el-option v-for="item in options.riskLevels" :key="item.value" :label="item.label" :value="item.value" />
+        </el-select>
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" @click="handleQuery">搜索</el-button>
-        <el-button @click="resetQuery">重置</el-button>
+        <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
+        <el-button :icon="Refresh" @click="resetQuery">重置</el-button>
       </el-form-item>
     </el-form>
 
-    <!-- 统计概览卡片 -->
+    <!-- ==================== 统计概览 ==================== -->
     <div class="stat-cards">
       <el-card class="stat-card stat-blue" shadow="hover">
         <div class="stat-card-inner">
-          <div class="stat-icon"><el-icon :size="32"><DataLine /></el-icon></div>
+          <div class="stat-icon"><el-icon :size="28"><DataLine /></el-icon></div>
           <div class="stat-info">
-            <div class="stat-value">{{ stats.total }}</div>
-            <div class="stat-label">总管网数</div>
+            <div class="stat-value">{{ stats.total ?? 0 }}</div>
+            <div class="stat-label">管网总数</div>
           </div>
         </div>
       </el-card>
       <el-card class="stat-card stat-red" shadow="hover">
         <div class="stat-card-inner">
-          <div class="stat-icon"><el-icon :size="32"><Warning /></el-icon></div>
+          <div class="stat-icon"><el-icon :size="28"><Warning /></el-icon></div>
           <div class="stat-info">
-            <div class="stat-value">{{ stats.withAlarm }}</div>
+            <div class="stat-value">{{ stats.withAlarm ?? 0 }}</div>
             <div class="stat-label">有报警管网</div>
           </div>
         </div>
       </el-card>
       <el-card class="stat-card stat-orange" shadow="hover">
         <div class="stat-card-inner">
-          <div class="stat-icon"><el-icon :size="32"><WarningFilled /></el-icon></div>
+          <div class="stat-icon"><el-icon :size="28"><WarningFilled /></el-icon></div>
           <div class="stat-info">
-            <div class="stat-value">{{ stats.totalAlarms }}</div>
+            <div class="stat-value">{{ stats.totalAlarms ?? 0 }}</div>
             <div class="stat-label">总报警数</div>
           </div>
         </div>
       </el-card>
       <el-card class="stat-card stat-green" shadow="hover">
         <div class="stat-card-inner">
-          <div class="stat-icon"><el-icon :size="32"><CircleCheck /></el-icon></div>
+          <div class="stat-icon"><el-icon :size="28"><CircleCheck /></el-icon></div>
           <div class="stat-info">
-            <div class="stat-value">{{ stats.noAlarm }}</div>
+            <div class="stat-value">{{ stats.noAlarm ?? 0 }}</div>
             <div class="stat-label">无报警管网</div>
           </div>
         </div>
       </el-card>
       <el-card class="stat-card stat-purple" shadow="hover">
         <div class="stat-card-inner">
-          <div class="stat-icon"><el-icon :size="32"><TrendCharts /></el-icon></div>
+          <div class="stat-icon"><el-icon :size="28"><TrendCharts /></el-icon></div>
           <div class="stat-info">
-            <div class="stat-value">{{ stats.highRisk }}</div>
+            <div class="stat-value">{{ stats.highRisk ?? 0 }}</div>
             <div class="stat-label">高风险管网</div>
           </div>
         </div>
       </el-card>
     </div>
 
-    <!-- 数据表格 -->
-    <el-table :data="tableData" border style="width: 100%" v-loading="loading">
-      <el-table-column label="序号" type="index" width="60" align="center" :index="(i) => (pageNum - 1) * pageSize + i + 1" />
-      <el-table-column label="管网名称" prop="name" min-width="180" />
-      <el-table-column label="所属区域" prop="area" min-width="120" />
-      <el-table-column label="管网类型" prop="networkType" min-width="100" align="center">
+    <!-- ==================== 风险等级分布条 ==================== -->
+    <div class="level-bar" v-if="(stats.total ?? 0) > 0">
+      <div class="level-bar-title">风险等级分布</div>
+      <div class="level-bar-track">
+        <div v-for="item in levelSegments" :key="item.name" class="level-segment"
+          :class="{ active: queryParams.riskLevel === item.value }" :style="{ flex: item.value_count || 0.0001, background: item.color }"
+          :title="`${item.name}:${item.value_count} 条(点击筛选)`" @click="toggleRiskLevel(item.value)">
+          <span class="segment-text" v-if="item.value_count > 0">{{ item.name }} {{ item.value_count }}</span>
+        </div>
+      </div>
+      <div class="level-bar-legend">
+        <span class="legend-item" v-for="item in levelSegments" :key="item.name">
+          <i :style="{ background: item.color }"></i>{{ item.name }} {{ item.value_count }}
+        </span>
+      </div>
+    </div>
+
+    <!-- ==================== 数据表格 ==================== -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="Refresh" :loading="loading" @click="loadList">刷新</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="loadList"></right-toolbar>
+    </el-row>
+
+    <el-table :data="tableData" style="width: 100%" v-loading="loading"
+      :default-sort="{ prop: 'riskScore', order: 'descending' }">
+      <el-table-column label="序号" type="index" width="60" align="center"
+        :index="(i) => (pageNum - 1) * pageSize + i + 1" />
+      <el-table-column prop="networkCode" label="管线编号" min-width="140" show-overflow-tooltip>
+        <template #default="{ row }">{{ row.networkCode || '-' }}</template>
+      </el-table-column>
+      <el-table-column prop="networkName" label="管网名称" min-width="150" show-overflow-tooltip>
+        <template #default="{ row }">{{ row.networkName || '-' }}</template>
+      </el-table-column>
+      <el-table-column label="管线类别" width="110" align="center">
         <template #default="{ row }">
-          <el-tag :type="networkTypeTagType(row.networkType)" effect="light">{{ row.networkType }}</el-tag>
+          <el-tag v-if="row.networkLevel" size="small" effect="plain">{{ row.networkLevel }}</el-tag>
+          <span v-else class="text-muted">-</span>
         </template>
       </el-table-column>
-      <el-table-column label="关联设备" prop="equipmentId" min-width="100" align="center">
+      <el-table-column prop="managementUnit" label="所属区域" min-width="130" show-overflow-tooltip>
+        <template #default="{ row }">{{ row.managementUnit || '-' }}</template>
+      </el-table-column>
+      <el-table-column prop="location" label="所在道路" min-width="170" show-overflow-tooltip>
+        <template #default="{ row }">{{ row.location || '-' }}</template>
+      </el-table-column>
+      <el-table-column label="关联设备" width="100" align="center">
         <template #default="{ row }">
-          <span v-if="row.equipmentId">已关联</span>
-          <span v-else style="color:#909399">未关联</span>
+          <span v-if="row.equipmentCount > 0">{{ row.equipmentCount }}</span>
+          <span v-else class="text-muted">未关联</span>
         </template>
       </el-table-column>
-      <el-table-column label="报警次数" min-width="100" align="center">
+      <el-table-column label="报警次数" width="100" align="center" prop="alarmCount" sortable>
         <template #default="{ row }">
-          <span :class="row._alarmCount > 0 ? 'alarm-has' : ''">{{ row._alarmCount ?? '-' }}</span>
+          <span :class="row.alarmCount > 0 ? 'alarm-has' : 'text-muted'">{{ row.alarmCount ?? 0 }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="风险等级" min-width="100" align="center">
+      <el-table-column label="风险等级" width="100" align="center" prop="riskScore" sortable>
         <template #default="{ row }">
-          <el-tag :type="riskTagType(row._riskLevel)" effect="light">{{ riskLevelName(row._riskLevel) }}</el-tag>
+          <el-tag :type="riskTagType(row.riskLevel)" size="small" effect="dark">{{ row.riskLevelName }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="风险评分" min-width="100" align="center">
+      <el-table-column label="风险评分" width="150" align="center">
         <template #default="{ row }">
-          <span :class="riskScoreClass(row._riskScore)">{{ row._riskScore }}</span>
+          <div class="score-cell">
+            <el-progress :percentage="row.riskScore || 0" :stroke-width="8" :show-text="false"
+              :color="row.riskColor" class="score-bar" />
+            <span class="score-num" :style="{ color: row.riskColor }">{{ row.riskScore }}</span>
+          </div>
         </template>
       </el-table-column>
-      <el-table-column label="创建时间" prop="createTime" min-width="160" align="center" />
+      <el-table-column label="评估时间" width="168" align="center">
+        <template #default="{ row }">{{ formatDateTime(row.assessTime) }}</template>
+      </el-table-column>
       <el-table-column label="操作" width="100" align="center" fixed="right">
         <template #default="{ row }">
           <el-button link type="primary" @click="handleDetail(row)">查看详情</el-button>
@@ -107,66 +162,18 @@
       </el-table-column>
     </el-table>
 
-    <!-- 分页 -->
+    <!-- ==================== 分页 ==================== -->
     <div class="pagination-wrap">
-      <el-pagination
-        v-model:current-page="pageNum"
-        v-model:page-size="pageSize"
-        :page-sizes="[10, 20, 50]"
-        :total="total"
-        layout="total, sizes, prev, pager, next, jumper"
-        background
-        @size-change="loadData"
-        @current-change="loadData"
-      />
+      <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize"
+        @pagination="loadList" />
     </div>
 
-    <!-- 详情弹窗 -->
-    <el-drawer v-model="detailVisible" title="管网综合风险详情" size="800px" destroy-on-close class="detail-drawer" direction="rtl">
-      <template v-if="currentRow">
-        <el-tabs v-model="detailTab">
-          <el-tab-pane label="基本信息" name="basic">
-            <el-descriptions :column="2" border>
-              <el-descriptions-item label="管网名称">{{ currentRow.name }}</el-descriptions-item>
-              <el-descriptions-item label="所属区域">{{ currentRow.area }}</el-descriptions-item>
-              <el-descriptions-item label="管网类型">
-                <el-tag :type="networkTypeTagType(currentRow.networkType)" effect="light">{{ currentRow.networkType }}</el-tag>
-              </el-descriptions-item>
-              <el-descriptions-item label="关联设备">{{ currentRow.equipmentId ? '已关联 (ID: ' + currentRow.equipmentId + ')' : '未关联' }}</el-descriptions-item>
-              <el-descriptions-item label="综合风险等级">
-                <el-tag :type="riskTagType(currentRow._riskLevel)" effect="light">{{ riskLevelName(currentRow._riskLevel) }}</el-tag>
-              </el-descriptions-item>
-              <el-descriptions-item label="综合风险评分">{{ currentRow._riskScore }} 分</el-descriptions-item>
-              <el-descriptions-item label="坐标信息" :span="2">{{ formatPoints(currentRow.points) }}</el-descriptions-item>
-              <el-descriptions-item label="创建时间">{{ currentRow.createTime }}</el-descriptions-item>
-              <el-descriptions-item label="更新时间">{{ currentRow.updateTime }}</el-descriptions-item>
-            </el-descriptions>
-          </el-tab-pane>
-          <el-tab-pane label="报警统计" name="alarm">
-            <div v-if="currentRow.equipmentId" v-loading="alarmLoading">
-              <div style="margin-bottom:16px">
-                <el-tag :type="currentRow._alarmCount > 0 ? 'danger' : 'success'" size="large">
-                  报警总数:{{ currentRow._alarmCount ?? 0 }}
-                </el-tag>
-              </div>
-              <el-table :data="alarmList" border size="small" v-if="alarmList.length > 0">
-                <el-table-column prop="createTime" label="报警时间" min-width="160" />
-                <el-table-column prop="alarmType" label="报警类型" min-width="100" />
-                <el-table-column prop="alarmContent" label="报警内容" min-width="200" />
-                <el-table-column prop="status" label="状态" min-width="80" align="center">
-                  <template #default="{ row }">
-                    <el-tag :type="row.status === 0 ? 'warning' : row.status === 1 ? 'primary' : 'success'" size="small">
-                      {{ row.status === 0 ? '未处理' : row.status === 1 ? '处理中' : '已处理' }}
-                    </el-tag>
-                  </template>
-                </el-table-column>
-              </el-table>
-              <el-empty v-else description="暂无报警数据" :image-size="60" />
-            </div>
-            <el-empty v-else description="该管网未关联设备,无报警数据" :image-size="60" />
-          </el-tab-pane>
-        </el-tabs>
-      </template>
+    <!-- ==================== 详情抽屉 ==================== -->
+    <el-drawer v-model="detailVisible" title="管网综合风险评估详情" size="900px" :close-on-click-modal="false"
+      class="risk-detail-drawer" direction="rtl">
+      <div v-loading="detailLoading" class="drawer-body">
+        <RiskAssessmentTabs v-if="detailData" :detail="detailData" />
+      </div>
       <template #footer>
         <el-button @click="detailVisible = false">关 闭</el-button>
       </template>
@@ -175,172 +182,157 @@
 </template>
 
 <script setup name="Zhfxtjfx">
-import { ref, computed, onMounted } from 'vue'
-import { useRouter } from 'vue-router'
-import { DataLine, Warning, WarningFilled, CircleCheck, TrendCharts } from '@element-plus/icons-vue'
-import { getPipeNetworkPage, getAlarmDataPage } from '@/api/drainage'
-
-const router = useRouter()
-
-// ==================== 下拉选项 ====================
-const networkTypeOptions = ['管线', '管点', '排口']
-
-// ==================== 搜索参数 ====================
-const queryParams = ref({ name: '', networkType: '管线', area: '' })
-const queryRef = ref(null)
+import { ref, reactive, computed, onMounted } from 'vue'
+import { Search, Refresh, DataLine, Warning, WarningFilled, CircleCheck, TrendCharts } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import {
+  getDrainageRiskPage,
+  getDrainageRiskStatistics,
+  getDrainageRiskFilterOptions,
+  getDrainageRiskDetail
+} from '@/api/drainageRisk'
+import RiskAssessmentTabs from './components/RiskAssessmentTabs.vue'
+
+// ==================== 筛选与分页 ====================
+const showSearch = ref(true)
+const queryParams = reactive({
+  networkCode: '',
+  networkLevel: '',
+  managementUnit: '',
+  location: '',
+  riskLevel: ''
+})
 
-// ==================== 分页与数据 ====================
 const pageNum = ref(1)
 const pageSize = ref(10)
 const total = ref(0)
 const tableData = ref([])
 const loading = ref(false)
 
-const handleQuery = () => {
-  pageNum.value = 1
-  loadData()
-}
+const options = reactive({
+  networkLevels: [],
+  managementUnits: [],
+  riskLevels: []
+})
 
-const resetQuery = () => {
-  queryParams.value = { name: '', networkType: '管线', area: '' }
-  pageNum.value = 1
-  loadData()
-}
-
-const getRiskScore = (item, alarmCount = 0) => {
-  // 评分完全由当前排水管网基础资料和实时报警数量计算,不使用演示数据。
-  const alarmScore = Math.min(70, Number(alarmCount || 0) * 10)
-  const equipmentScore = item.equipmentId ? 0 : 15
-  const coordinateScore = item.points ? 0 : 15
-  return Math.min(100, alarmScore + equipmentScore + coordinateScore)
-}
-
-const getRiskLevel = score => score >= 70 ? 'high' : score >= 40 ? 'medium' : 'low'
-const riskLevelName = level => level === 'high' ? '高风险' : level === 'medium' ? '中风险' : '低风险'
-const riskTagType = level => level === 'high' ? 'danger' : level === 'medium' ? 'warning' : 'success'
-const riskScoreClass = score => score >= 70 ? 'risk-score-high' : score >= 40 ? 'risk-score-medium' : 'risk-score-low'
-
-const getEquipmentIds = equipmentId => String(equipmentId || '').split(',').map(id => id.trim()).filter(Boolean)
-
-const getAlarmCountByEquipment = async equipmentId => {
-  const ids = getEquipmentIds(equipmentId)
-  if (!ids.length) return 0
-  const counts = await Promise.all(ids.map(async id => {
-    try {
-      const alarmRes = await getAlarmDataPage(1, 1, { equipmentId: id, equipmentType: 'drainage' })
-      const page = alarmRes.code !== undefined ? alarmRes.data : alarmRes
-      return Number(page?.total || 0)
-    } catch (e) {
-      return 0
+const stats = ref({})
+
+/** 组装查询条件,空值不传,避免后端把空串当筛选条件 */
+function buildParams() {
+  const params = {}
+  Object.keys(queryParams).forEach((key) => {
+    const value = queryParams[key]
+    if (value !== '' && value !== null && value !== undefined) {
+      params[key] = value
     }
+  })
+  return params
+}
+
+// ==================== 风险等级分布条 ====================
+const levelSegments = computed(() => {
+  const list = stats.value.levelDistribution || []
+  return list.map((item) => ({
+    name: item.name,
+    value: item.name === '高风险' ? 'high' : item.name === '中风险' ? 'medium' : 'low',
+    value_count: item.value || 0,
+    color: item.color
   }))
-  return counts.reduce((sum, count) => sum + count, 0)
+})
+
+function toggleRiskLevel(level) {
+  queryParams.riskLevel = queryParams.riskLevel === level ? '' : level
+  handleQuery()
 }
 
-// ==================== 加载数据 ====================
-const loadData = async () => {
+// ==================== 数据加载 ====================
+async function loadList() {
   loading.value = true
   try {
-    const params = {}
-    if (queryParams.value.name) params.name = queryParams.value.name
-    if (queryParams.value.networkType) params.networkType = queryParams.value.networkType
-    if (queryParams.value.area) params.area = queryParams.value.area
-
-    const res = await getPipeNetworkPage(pageNum.value, pageSize.value, params)
-    const pageData = res.code !== undefined ? res.data : res
-    const rows = pageData.records || pageData.rows || []
-    const totalCount = pageData.total ?? 0
-    total.value = totalCount
-
-    // 为每条管网查询报警数量
-    const withAlarms = await Promise.all(
-      rows.map(async (item) => {
-        const alarmCount = await getAlarmCountByEquipment(item.equipmentId)
-        const riskScore = getRiskScore(item, alarmCount)
-        return { ...item, _alarmCount: alarmCount, _riskScore: riskScore, _riskLevel: getRiskLevel(riskScore) }
-      })
-    )
-    tableData.value = withAlarms
-  } catch (e) {
-    console.error('加载管网数据失败', e)
+    const res = await getDrainageRiskPage({ ...buildParams(), pageNum: pageNum.value, pageSize: pageSize.value })
+    const page = res.code !== undefined ? res.data : res
+    tableData.value = page?.records || []
+    total.value = page?.total || 0
+  } catch (error) {
+    console.error('加载综合风险列表失败', error)
+    ElMessage.error('加载综合风险列表失败')
     tableData.value = []
+    total.value = 0
   } finally {
     loading.value = false
   }
 }
 
-// ==================== 统计 ====================
-const stats = computed(() => {
-  const list = tableData.value
-  const withAlarm = list.filter(i => i._alarmCount > 0).length
-  const totalAlarms = list.reduce((sum, i) => sum + (i._alarmCount || 0), 0)
-  const highRisk = list.filter(i => i._riskLevel === 'high').length
-  const mediumRisk = list.filter(i => i._riskLevel === 'medium').length
-  const lowRisk = list.filter(i => i._riskLevel === 'low').length
-  return {
-    total: total.value,
-    withAlarm,
-    totalAlarms,
-    noAlarm: list.length - withAlarm,
-    highRisk,
-    mediumRisk,
-    lowRisk
+async function loadStatistics() {
+  try {
+    const res = await getDrainageRiskStatistics(buildParams())
+    stats.value = (res.code !== undefined ? res.data : res) || {}
+  } catch (error) {
+    console.error('加载综合风险统计失败', error)
   }
-})
-
-// ==================== 工具函数 ====================
-const networkTypeTagType = (type) => {
-  const map = { '管线': '', '管点': 'warning', '排口': 'success' }
-  return map[type] || ''
 }
 
-const formatPoints = (points) => {
-  if (!points) return '暂无坐标'
+async function loadOptions() {
   try {
-    const parsed = typeof points === 'string' ? JSON.parse(points) : points
-    if (Array.isArray(parsed)) {
-      return parsed.map(p => `${p.lng || p[0]}, ${p.lat || p[1]}`).join(' → ')
-    }
-    return String(points)
-  } catch {
-    return String(points)
+    const res = await getDrainageRiskFilterOptions()
+    const data = (res.code !== undefined ? res.data : res) || {}
+    options.networkLevels = data.networkLevels || []
+    options.managementUnits = data.managementUnits || []
+    options.riskLevels = data.riskLevels || []
+  } catch (error) {
+    console.error('加载筛选选项失败', error)
   }
 }
 
-// ==================== 详情弹窗 ====================
+function handleQuery() {
+  pageNum.value = 1
+  loadList()
+  loadStatistics()
+}
+
+function resetQuery() {
+  queryParams.networkCode = ''
+  queryParams.networkLevel = ''
+  queryParams.managementUnit = ''
+  queryParams.location = ''
+  queryParams.riskLevel = ''
+  handleQuery()
+}
+
+// ==================== 详情 ====================
 const detailVisible = ref(false)
-const currentRow = ref(null)
-const detailTab = ref('basic')
-const alarmLoading = ref(false)
-const alarmList = ref([])
-
-const handleDetail = async (row) => {
-  currentRow.value = row
-  detailTab.value = 'basic'
+const detailLoading = ref(false)
+const detailData = ref(null)
+
+async function handleDetail(row) {
+  detailData.value = null
   detailVisible.value = true
-  alarmList.value = []
-
-  if (row.equipmentId) {
-    alarmLoading.value = true
-    try {
-      const ids = getEquipmentIds(row.equipmentId)
-      const results = await Promise.all(ids.map(id => getAlarmDataPage(1, 20, { equipmentId: id, equipmentType: 'drainage' })))
-      alarmList.value = results.flatMap(res => {
-        const page = res.code !== undefined ? res.data : res
-        if (Array.isArray(page)) return page
-        return page?.records || page?.rows || []
-      }).sort((a, b) => String(b.createTime || '').localeCompare(String(a.createTime || '')))
-    } catch (e) {
-      alarmList.value = []
-    } finally {
-      alarmLoading.value = false
-    }
+  detailLoading.value = true
+  try {
+    const res = await getDrainageRiskDetail(row.networkId)
+    detailData.value = (res.code !== undefined ? res.data : res) || null
+  } catch (error) {
+    console.error('加载风险评估详情失败', error)
+    ElMessage.error('加载风险评估详情失败')
+  } finally {
+    detailLoading.value = false
   }
 }
 
+// ==================== 工具 ====================
+function riskTagType(level) {
+  return level === 'high' ? 'danger' : level === 'medium' ? 'warning' : 'success'
+}
+
+function formatDateTime(value) {
+  if (!value) return '-'
+  return String(value).replace('T', ' ').slice(0, 19)
+}
+
 // ==================== 初始化 ====================
-onMounted(() => {
-  loadData()
+onMounted(async () => {
+  await loadOptions()
+  await Promise.all([loadList(), loadStatistics()])
 })
 </script>
 
@@ -349,16 +341,15 @@ onMounted(() => {
   padding: 20px;
 }
 
-/* 搜索表单 */
 .search-form {
-  margin-bottom: 16px;
+  margin-bottom: 8px;
 }
 
-/* 统计卡片 */
+/* ==================== 统计卡片 ==================== */
 .stat-cards {
   display: flex;
   gap: 16px;
-  margin-bottom: 20px;
+  margin-bottom: 16px;
 }
 
 .stat-card {
@@ -367,18 +358,18 @@ onMounted(() => {
 }
 
 .stat-card :deep(.el-card__body) {
-  padding: 16px 20px;
+  padding: 14px 18px;
 }
 
 .stat-card-inner {
   display: flex;
   align-items: center;
-  gap: 16px;
+  gap: 14px;
 }
 
 .stat-icon {
-  width: 56px;
-  height: 56px;
+  width: 50px;
+  height: 50px;
   border-radius: 12px;
   display: flex;
   align-items: center;
@@ -386,25 +377,11 @@ onMounted(() => {
   flex-shrink: 0;
 }
 
-.stat-blue .stat-icon {
-  background: rgba(64, 158, 255, 0.1);
-  color: #409EFF;
-}
-
-.stat-red .stat-icon {
-  background: rgba(245, 108, 108, 0.1);
-  color: #F56C6C;
-}
-
-.stat-orange .stat-icon {
-  background: rgba(230, 162, 60, 0.1);
-  color: #E6A23C;
-}
-
-.stat-green .stat-icon {
-  background: rgba(103, 194, 58, 0.1);
-  color: #67C23A;
-}
+.stat-blue .stat-icon { background: rgba(64, 158, 255, 0.1); color: #409EFF; }
+.stat-red .stat-icon { background: rgba(245, 108, 108, 0.1); color: #F56C6C; }
+.stat-orange .stat-icon { background: rgba(230, 162, 60, 0.1); color: #E6A23C; }
+.stat-green .stat-icon { background: rgba(103, 194, 58, 0.1); color: #67C23A; }
+.stat-purple .stat-icon { background: rgba(144, 97, 255, 0.1); color: #9061ff; }
 
 .stat-info {
   flex: 1;
@@ -412,43 +389,134 @@ onMounted(() => {
 }
 
 .stat-value {
-  font-size: 28px;
+  font-size: 26px;
   font-weight: 700;
   line-height: 1.2;
+  font-variant-numeric: tabular-nums;
 }
 
 .stat-blue .stat-value { color: #409EFF; }
 .stat-red .stat-value { color: #F56C6C; }
 .stat-orange .stat-value { color: #E6A23C; }
 .stat-green .stat-value { color: #67C23A; }
-
-.stat-purple .stat-icon {
-  background: rgba(144, 97, 255, 0.1);
-  color: #9061ff;
-}
-
 .stat-purple .stat-value { color: #9061ff; }
 
 .stat-label {
-  font-size: 14px;
+  font-size: 13px;
   color: #909399;
-  margin-top: 4px;
+  margin-top: 2px;
+}
+
+/* ==================== 风险等级分布条 ==================== */
+.level-bar {
+  background: #fff;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 12px 16px;
+  margin-bottom: 16px;
+}
+
+.level-bar-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 8px;
+}
+
+.level-bar-track {
+  display: flex;
+  height: 22px;
+  border-radius: 4px;
+  overflow: hidden;
+  background: #f5f7fa;
+}
+
+.level-segment {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  transition: opacity 0.15s, filter 0.15s;
+  min-width: 2px;
+  overflow: hidden;
+}
+
+.level-segment:hover {
+  filter: brightness(1.08);
+}
+
+.level-segment.active {
+  box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.85);
+}
+
+.segment-text {
+  font-size: 11px;
+  color: #fff;
+  white-space: nowrap;
+  padding: 0 6px;
 }
 
-/* 报警数高亮 */
+.level-bar-legend {
+  display: flex;
+  gap: 20px;
+  margin-top: 8px;
+}
+
+.legend-item {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  font-size: 12px;
+  color: #606266;
+}
+
+.legend-item i {
+  width: 8px;
+  height: 8px;
+  border-radius: 2px;
+  display: inline-block;
+}
+
+/* ==================== 表格 ==================== */
 .alarm-has {
   color: #F56C6C;
   font-weight: 700;
 }
 
-.risk-score-high { color: #F56C6C; font-weight: 700; }
-.risk-score-medium { color: #E6A23C; font-weight: 700; }
-.risk-score-low { color: #67C23A; font-weight: 700; }
+.text-muted {
+  color: #c0c4cc;
+}
+
+.score-cell {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.score-bar {
+  flex: 1;
+  min-width: 0;
+}
+
+.score-num {
+  font-weight: 700;
+  font-variant-numeric: tabular-nums;
+  width: 26px;
+  text-align: right;
+  flex-shrink: 0;
+}
 
-/* 分页 */
 .pagination-wrap {
   display: flex;
   justify-content: flex-end;
   margin-top: 16px;
 }
+
+.drawer-body {
+  min-height: 200px;
+}
+
+.risk-detail-drawer :deep(.el-drawer__body) {
+  padding-top: 12px;
+}
 </style>

+ 163 - 348
src/views/subSystem/drainage/fxpg/zhfxxqfx.vue

@@ -1,355 +1,154 @@
 <template>
-  <div class="app-container">
-    <!-- 顶部搜索区 -->
-    <el-form :model="searchKey" :inline="true" class="search-form">
+  <div class="app-container risk-detail-page">
+    <!-- ==================== 搜索区 ==================== -->
+    <el-form :model="searchKey" :inline="true" class="search-form" @submit.prevent>
+      <el-form-item label="管线编号">
+        <el-input v-model="searchKey.networkCode" placeholder="请输入" clearable style="width: 160px"
+          @keyup.enter="handleSearch" />
+      </el-form-item>
       <el-form-item label="管网名称">
-        <el-input v-model="searchKey.name" placeholder="请输入管网名称" clearable style="width: 220px" />
+        <el-input v-model="searchKey.networkName" placeholder="请输入" clearable style="width: 160px"
+          @keyup.enter="handleSearch" />
       </el-form-item>
-      <el-form-item label="管网类型">
-        <el-select v-model="searchKey.networkType" placeholder="请选择" clearable style="width: 140px">
-          <el-option v-for="item in networkTypeOptions" :key="item" :label="item" :value="item" />
+      <el-form-item label="风险级别">
+        <el-select v-model="searchKey.riskLevel" placeholder="全部" clearable style="width: 130px">
+          <el-option label="高风险" value="high" />
+          <el-option label="中风险" value="medium" />
+          <el-option label="低风险" value="low" />
         </el-select>
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" @click="handleSearch">搜索</el-button>
+        <el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
+        <el-button :icon="Refresh" @click="resetSearch">重置</el-button>
       </el-form-item>
     </el-form>
 
     <div class="main-layout">
-      <!-- 左侧管网列表 -->
+      <!-- ==================== 左侧管网列表 ==================== -->
       <div class="left-panel">
-        <div class="panel-title">管网列表</div>
+        <div class="panel-title">
+          <span>管网列表</span>
+          <span class="panel-count">{{ pipeList.length }} 条</span>
+        </div>
         <div class="pipe-list" v-loading="listLoading">
-          <div v-for="item in pipeList" :key="item.id" class="pipe-item"
-            :class="{ active: currentPipe?.id === item.id }" @click="selectPipe(item)">
-            <div class="pipe-item-name">{{ item.name }}</div>
-            <div class="pipe-item-meta">
-              <el-tag size="small" :type="networkTypeTagType(item.networkType)" effect="light">{{ item.networkType }}</el-tag>
-              <span class="pipe-item-area">{{ item.area }}</span>
+          <div v-for="item in pipeList" :key="item.networkId" class="pipe-item"
+            :class="{ active: currentPipe?.networkId === item.networkId }" @click="selectPipe(item)">
+            <div class="pipe-item-head">
+              <span class="pipe-item-name" :title="item.networkName">{{ item.networkName || '-' }}</span>
+              <el-tag size="small" :type="riskTagType(item.riskLevel)" effect="dark">{{ item.riskLevelName }}</el-tag>
             </div>
-            <div class="pipe-item-risk">
-              <el-tag size="small" :type="riskTagType(item._riskLevel)">{{ riskLevelName(item._riskLevel) }}</el-tag>
-              <span :class="riskScoreClass(item._riskScore)">{{ item._riskScore }}分</span>
+            <div class="pipe-item-meta">
+              <span class="meta-code">{{ item.networkCode || '-' }}</span>
+              <span v-if="item.networkLevel" class="meta-tag">{{ item.networkLevel }}</span>
             </div>
-            <div v-if="item._alarmCount > 0" class="pipe-item-alarm">
-              <el-icon color="#F56C6C"><Warning /></el-icon>
-              <span>{{ item._alarmCount }} 条报警</span>
+            <div class="pipe-item-sub" :title="item.location">{{ item.location || '未登记所在道路' }}</div>
+            <div class="pipe-item-foot">
+              <span class="foot-score" :style="{ color: item.riskColor }">{{ item.riskScore }} 分</span>
+              <span v-if="item.alarmCount > 0" class="foot-alarm">
+                <el-icon><Warning /></el-icon>{{ item.alarmCount }} 条报警
+              </span>
             </div>
           </div>
-          <el-empty v-if="pipeList.length === 0 && !listLoading" description="暂无数据" :image-size="60" />
+          <el-empty v-if="pipeList.length === 0 && !listLoading" description="暂无排水管网数据" :image-size="70" />
         </div>
       </div>
 
-      <!-- 右侧详情区 -->
+      <!-- ==================== 右侧详情 ==================== -->
       <div class="right-panel">
-        <template v-if="currentPipe">
-          <el-tabs v-model="activeTab" type="border-card">
-            <!-- Tab 1: 基本信息 -->
-            <el-tab-pane label="基本信息" name="basic">
-              <el-descriptions :column="2" border>
-                <el-descriptions-item label="管网名称">{{ currentPipe.name }}</el-descriptions-item>
-                <el-descriptions-item label="所属区域">{{ currentPipe.area }}</el-descriptions-item>
-                <el-descriptions-item label="管网类型">
-                  <el-tag :type="networkTypeTagType(currentPipe.networkType)" effect="light">{{ currentPipe.networkType }}</el-tag>
-                </el-descriptions-item>
-                <el-descriptions-item label="关联设备">
-                  <span v-if="currentPipe.equipmentId">已关联 (ID: {{ currentPipe.equipmentId }})</span>
-                  <span v-else style="color:#909399">未关联</span>
-                </el-descriptions-item>
-                <el-descriptions-item label="综合风险等级"><el-tag :type="riskTagType(currentPipe._riskLevel)">{{ riskLevelName(currentPipe._riskLevel) }}</el-tag></el-descriptions-item>
-                <el-descriptions-item label="综合风险评分">{{ currentPipe._riskScore }} 分</el-descriptions-item>
-                <el-descriptions-item label="坐标信息" :span="2">{{ formatPoints(currentPipe.points) }}</el-descriptions-item>
-                <el-descriptions-item label="创建时间">{{ currentPipe.createTime }}</el-descriptions-item>
-                <el-descriptions-item label="更新时间">{{ currentPipe.updateTime }}</el-descriptions-item>
-              </el-descriptions>
-            </el-tab-pane>
-
-            <!-- Tab 2: 报警统计 -->
-            <el-tab-pane label="报警统计" name="alarm">
-              <div v-if="currentPipe.equipmentId" v-loading="alarmLoading">
-                <!-- 报警统计概览 -->
-                <div class="alarm-stats-bar">
-                  <div class="alarm-stat-item">
-                    <span class="alarm-stat-value">{{ alarmTotal }}</span>
-                    <span class="alarm-stat-label">总报警数</span>
-                  </div>
-                  <div class="alarm-stat-item">
-                    <span class="alarm-stat-value" style="color:#F56C6C">{{ alarmUnhandled }}</span>
-                    <span class="alarm-stat-label">未处理</span>
-                  </div>
-                  <div class="alarm-stat-item">
-                    <span class="alarm-stat-value" style="color:#E6A23C">{{ alarmHandling }}</span>
-                    <span class="alarm-stat-label">处理中</span>
-                  </div>
-                  <div class="alarm-stat-item">
-                    <span class="alarm-stat-value" style="color:#67C23A">{{ alarmHandled }}</span>
-                    <span class="alarm-stat-label">已处理</span>
-                  </div>
-                </div>
-
-                <!-- 报警列表 -->
-                <el-table :data="alarmList" border style="width: 100%; margin-top: 16px;" v-if="alarmList.length > 0">
-                  <el-table-column label="序号" type="index" width="60" align="center" />
-                  <el-table-column prop="createTime" label="报警时间" min-width="160" />
-                  <el-table-column prop="alarmType" label="报警类型" min-width="100" />
-                  <el-table-column prop="alarmContent" label="报警内容" min-width="220" />
-                  <el-table-column prop="status" label="状态" min-width="90" align="center">
-                    <template #default="{ row }">
-                      <el-tag :type="row.status === 0 ? 'warning' : row.status === 1 ? 'primary' : 'success'" size="small">
-                        {{ row.status === 0 ? '未处理' : row.status === 1 ? '处理中' : '已处理' }}
-                      </el-tag>
-                    </template>
-                  </el-table-column>
-                </el-table>
-                <el-empty v-else description="暂无报警数据" :image-size="60" />
-
-                <!-- 报警分页 -->
-                <div v-if="alarmTotal > 0" class="alarm-pagination">
-                  <el-pagination
-                    v-model:current-page="alarmPageNum"
-                    v-model:page-size="alarmPageSize"
-                    :page-sizes="[10, 20, 50]"
-                    :total="alarmTotal"
-                    layout="total, sizes, prev, pager, next"
-                    small
-                    background
-                    @size-change="loadAlarmData"
-                    @current-change="loadAlarmData"
-                  />
-                </div>
-              </div>
-              <el-empty v-else description="该管网未关联设备,无报警数据" :image-size="80" />
-            </el-tab-pane>
-
-            <!-- Tab 3: 报警趋势 -->
-            <el-tab-pane label="报警趋势" name="trend">
-              <div v-if="currentPipe.equipmentId && alarmTrendData.length > 0">
-                <div ref="trendChartRef" style="height: 350px;"></div>
-              </div>
-              <el-empty v-else description="暂无报警趋势数据" :image-size="80" />
-            </el-tab-pane>
-          </el-tabs>
-        </template>
-        <el-empty v-else description="请选择左侧管网查看详情" :image-size="120" />
+        <div class="detail-scroll" v-loading="detailLoading">
+          <RiskAssessmentTabs v-if="detailData" :detail="detailData" />
+          <el-empty v-else-if="!detailLoading" description="请选择左侧管网查看综合风险评估详情" :image-size="120" />
+        </div>
       </div>
     </div>
   </div>
 </template>
 
 <script setup name="Zhfxxqfx">
-import { ref, computed, watch, nextTick, onMounted } from 'vue'
+import { ref, reactive, onMounted } from 'vue'
 import { useRoute } from 'vue-router'
-import { Warning } from '@element-plus/icons-vue'
-import * as echarts from 'echarts'
-import { getPipeNetworkPage, getAlarmDataPage } from '@/api/drainage'
+import { Search, Refresh, Warning } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { getDrainageRiskPage, getDrainageRiskDetail } from '@/api/drainageRisk'
+import RiskAssessmentTabs from './components/RiskAssessmentTabs.vue'
 
 const route = useRoute()
 
-// ==================== 选项 ====================
-const networkTypeOptions = ['管线', '管点', '排口']
-
 // ==================== 搜索 ====================
-const searchKey = ref({ name: '', networkType: '管线' })
-const handleSearch = () => { loadPipeList() }
-
-const getEquipmentIds = equipmentId => String(equipmentId || '').split(',').map(id => id.trim()).filter(Boolean)
-const getRiskScore = (item, alarmCount) => Math.min(100,
-  Math.min(70, Number(alarmCount || 0) * 10) + (item.equipmentId ? 0 : 15) + (item.points ? 0 : 15))
-const getRiskLevel = score => score >= 70 ? 'high' : score >= 40 ? 'medium' : 'low'
-const riskLevelName = level => level === 'high' ? '高风险' : level === 'medium' ? '中风险' : '低风险'
-const riskTagType = level => level === 'high' ? 'danger' : level === 'medium' ? 'warning' : 'success'
-const riskScoreClass = score => score >= 70 ? 'risk-score-high' : score >= 40 ? 'risk-score-medium' : 'risk-score-low'
+const searchKey = reactive({ networkCode: '', networkName: '', riskLevel: '' })
 
 // ==================== 管网列表 ====================
 const pipeList = ref([])
 const listLoading = ref(false)
 const currentPipe = ref(null)
-const activeTab = ref('basic')
 
-const loadPipeList = async () => {
+async function loadPipeList() {
   listLoading.value = true
   try {
-    const params = {}
-    if (searchKey.value.name) params.name = searchKey.value.name
-    if (searchKey.value.networkType) params.networkType = searchKey.value.networkType
-
-    // 加载较多数据用于左侧列表
-    const res = await getPipeNetworkPage(1, 200, params)
-    const pageData = res.code !== undefined ? res.data : res
-    const rows = pageData.records || pageData.rows || []
-
-    // 批量查询报警数
-    const withAlarms = await Promise.all(
-      rows.map(async (item) => {
-        let alarmCount = 0
-        const ids = getEquipmentIds(item.equipmentId)
-        const counts = await Promise.all(ids.map(async id => {
-          try {
-            const alarmRes = await getAlarmDataPage(1, 1, { equipmentId: id, equipmentType: 'drainage' })
-            const alarmPageData = alarmRes.code !== undefined ? alarmRes.data : alarmRes
-            return Number(alarmPageData?.total || 0)
-          } catch (e) { return 0 }
-        }))
-        alarmCount = counts.reduce((sum, count) => sum + count, 0)
-        const riskScore = getRiskScore(item, alarmCount)
-        return { ...item, _alarmCount: alarmCount, _riskScore: riskScore, _riskLevel: getRiskLevel(riskScore) }
-      })
-    )
-    pipeList.value = withAlarms
-
-    // 如果有路由参数指定ID,自动选中
+    const params = { pageNum: 1, pageSize: 200 }
+    if (searchKey.networkCode) params.networkCode = searchKey.networkCode
+    if (searchKey.networkName) params.networkName = searchKey.networkName
+    if (searchKey.riskLevel) params.riskLevel = searchKey.riskLevel
+
+    const res = await getDrainageRiskPage(params)
+    const page = res.code !== undefined ? res.data : res
+    pipeList.value = page?.records || []
+
+    // 优先按路由参数选中,其次选中第一条
     const queryId = route.query.id
-    if (queryId) {
-      const found = withAlarms.find(i => String(i.id) === String(queryId))
-      if (found) selectPipe(found)
-    } else if (withAlarms.length > 0 && !currentPipe.value) {
-      selectPipe(withAlarms[0])
+    const target = queryId
+      ? pipeList.value.find((item) => String(item.networkId) === String(queryId))
+      : null
+    if (target) {
+      selectPipe(target)
+    } else if (!currentPipe.value && pipeList.value.length > 0) {
+      selectPipe(pipeList.value[0])
     }
-  } catch (e) {
-    console.error('加载管网列表失败', e)
+  } catch (error) {
+    console.error('加载排水管网风险列表失败', error)
+    ElMessage.error('加载排水管网风险列表失败')
     pipeList.value = []
   } finally {
     listLoading.value = false
   }
 }
 
-const selectPipe = (item) => {
-  currentPipe.value = item
-  activeTab.value = 'basic'
-  // 重置报警数据
-  alarmList.value = []
-  alarmTotal.value = 0
-  alarmPageNum.value = 1
-  alarmTrendData.value = []
-}
-
-// ==================== 报警数据 ====================
-const alarmLoading = ref(false)
-const alarmList = ref([])
-const alarmTotal = ref(0)
-const alarmPageNum = ref(1)
-const alarmPageSize = ref(10)
-
-const alarmUnhandled = computed(() => alarmList.value.filter(i => i.status === 0).length)
-const alarmHandling = computed(() => alarmList.value.filter(i => i.status === 1).length)
-const alarmHandled = computed(() => alarmList.value.filter(i => i.status === 2).length)
-
-const queryAlarmsForPipe = async (equipmentId, pageSize) => {
-  const ids = getEquipmentIds(equipmentId)
-  if (!ids.length) return { rows: [], total: 0 }
-  const result = await Promise.all(ids.map(async id => {
-    const res = await getAlarmDataPage(1, pageSize, { equipmentId: id, equipmentType: 'drainage' })
-    const page = res.code !== undefined ? res.data : res
-    return { rows: page?.records || page?.rows || [], total: Number(page?.total || 0) }
-  }))
-  return {
-    rows: result.flatMap(item => item.rows).sort((a, b) => String(b.createTime || '').localeCompare(String(a.createTime || ''))),
-    total: result.reduce((sum, item) => sum + item.total, 0)
-  }
+function handleSearch() {
+  currentPipe.value = null
+  detailData.value = null
+  loadPipeList()
 }
 
-const loadAlarmData = async () => {
-  if (!currentPipe.value?.equipmentId) return
-  alarmLoading.value = true
-  try {
-    const result = await queryAlarmsForPipe(currentPipe.value.equipmentId, alarmPageSize.value)
-    const start = (alarmPageNum.value - 1) * alarmPageSize.value
-    alarmList.value = result.rows.slice(start, start + alarmPageSize.value)
-    alarmTotal.value = result.total
-  } catch (e) {
-    alarmList.value = []
-    alarmTotal.value = 0
-  } finally {
-    alarmLoading.value = false
-  }
+function resetSearch() {
+  searchKey.networkCode = ''
+  searchKey.networkName = ''
+  searchKey.riskLevel = ''
+  handleSearch()
 }
 
-// ==================== 报警趋势 ====================
-const alarmTrendData = ref([])
-const trendChartRef = ref(null)
-let trendChart = null
+// ==================== 详情 ====================
+const detailData = ref(null)
+const detailLoading = ref(false)
 
-const loadAlarmTrend = async () => {
-  if (!currentPipe.value?.equipmentId) return
+async function selectPipe(item) {
+  if (!item) return
+  currentPipe.value = item
+  detailLoading.value = true
   try {
-    // 加载较多报警数据用于趋势分析
-    const result = await queryAlarmsForPipe(currentPipe.value.equipmentId, 500)
-    const rows = result.rows
-    // 按日期聚合报警数
-    const dateMap = {}
-    rows.forEach(item => {
-      const date = (item.createTime || '').substring(0, 10)
-      if (date) {
-        dateMap[date] = (dateMap[date] || 0) + 1
-      }
-    })
-    alarmTrendData.value = Object.entries(dateMap)
-      .sort((a, b) => a[0].localeCompare(b[0]))
-      .map(([date, count]) => ({ date, count }))
-  } catch (e) {
-    alarmTrendData.value = []
-  }
-}
-
-const initTrendChart = () => {
-  if (!trendChartRef.value || alarmTrendData.value.length === 0) return
-  if (trendChart) { trendChart.dispose(); trendChart = null }
-  trendChart = echarts.init(trendChartRef.value)
-  trendChart.setOption({
-    tooltip: { trigger: 'axis' },
-    grid: { left: 50, right: 30, top: 30, bottom: 30 },
-    xAxis: {
-      type: 'category',
-      data: alarmTrendData.value.map(d => d.date),
-      axisLabel: { fontSize: 11, color: '#909399', rotate: 30 }
-    },
-    yAxis: {
-      type: 'value',
-      minInterval: 1,
-      axisLabel: { color: '#909399' },
-      splitLine: { lineStyle: { type: 'dashed' } }
-    },
-    series: [{
-      type: 'bar',
-      data: alarmTrendData.value.map(d => d.count),
-      itemStyle: { color: '#E6A23C', borderRadius: [4, 4, 0, 0] },
-      barMaxWidth: 30
-    }]
-  })
-}
-
-// ==================== 监听Tab切换 ====================
-watch(activeTab, (val) => {
-  if (val === 'alarm' && currentPipe.value?.equipmentId) {
-    loadAlarmData()
-  }
-  if (val === 'trend' && currentPipe.value?.equipmentId) {
-    loadAlarmTrend().then(() => nextTick(() => initTrendChart()))
+    const res = await getDrainageRiskDetail(item.networkId)
+    detailData.value = (res.code !== undefined ? res.data : res) || null
+  } catch (error) {
+    console.error('加载综合风险评估详情失败', error)
+    detailData.value = null
+    ElMessage.error('加载综合风险评估详情失败')
+  } finally {
+    detailLoading.value = false
   }
-})
-
-// 监听选中管网变化时销毁图表
-watch(currentPipe, () => {
-  if (trendChart) { trendChart.dispose(); trendChart = null }
-})
-
-// ==================== 工具函数 ====================
-const networkTypeTagType = (type) => {
-  const map = { '管线': '', '管点': 'warning', '排口': 'success' }
-  return map[type] || ''
 }
 
-const formatPoints = (points) => {
-  if (!points) return '暂无坐标'
-  try {
-    const parsed = typeof points === 'string' ? JSON.parse(points) : points
-    if (Array.isArray(parsed)) {
-      return parsed.map(p => `${p.lng || p[0]}, ${p.lat || p[1]}`).join(' → ')
-    }
-    return String(points)
-  } catch {
-    return String(points)
-  }
+// ==================== 工具 ====================
+function riskTagType(level) {
+  return level === 'high' ? 'danger' : level === 'medium' ? 'warning' : 'success'
 }
 
 // ==================== 初始化 ====================
@@ -363,35 +162,45 @@ onMounted(() => {
   padding: 20px;
 }
 
-/* 搜索表单 */
 .search-form {
-  margin-bottom: 16px;
+  margin-bottom: 12px;
 }
 
-/* 主布局 */
 .main-layout {
   display: flex;
   gap: 16px;
-  min-height: calc(100vh - 160px);
+  align-items: stretch;
+  min-height: calc(100vh - 180px);
 }
 
-/* 左侧面板 */
+/* ==================== 左侧面板 ==================== */
 .left-panel {
-  width: 280px;
+  width: 300px;
   flex-shrink: 0;
   background: #fff;
   border-radius: 4px;
   border: 1px solid #e4e7ed;
   display: flex;
   flex-direction: column;
+  overflow: hidden;
 }
 
 .panel-title {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
   padding: 12px 16px;
   font-size: 15px;
   font-weight: 600;
   color: #303133;
   border-bottom: 1px solid #e4e7ed;
+  flex-shrink: 0;
+}
+
+.panel-count {
+  font-size: 12px;
+  font-weight: 400;
+  color: #909399;
 }
 
 .pipe-list {
@@ -418,11 +227,21 @@ onMounted(() => {
   border-color: #409EFF;
 }
 
+.pipe-item-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 8px;
+  margin-bottom: 6px;
+}
+
 .pipe-item-name {
   font-size: 14px;
   color: #303133;
   font-weight: 500;
-  margin-bottom: 6px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
 .pipe-item-meta {
@@ -430,71 +249,67 @@ onMounted(() => {
   align-items: center;
   gap: 8px;
   margin-bottom: 4px;
+  font-size: 12px;
+}
+
+.meta-code {
+  color: #606266;
+  font-family: Consolas, Monaco, monospace;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
-.pipe-item-area {
+.meta-tag {
+  flex-shrink: 0;
+  color: #409EFF;
+  background: rgba(64, 158, 255, 0.1);
+  border-radius: 3px;
+  padding: 0 5px;
+}
+
+.pipe-item-sub {
   font-size: 12px;
   color: #909399;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-bottom: 6px;
 }
 
-.pipe-item-risk {
+.pipe-item-foot {
   display: flex;
   align-items: center;
-  gap: 8px;
-  margin-top: 6px;
+  justify-content: space-between;
   font-size: 12px;
 }
 
-.risk-score-high { color: #F56C6C; font-weight: 700; }
-.risk-score-medium { color: #E6A23C; font-weight: 700; }
-.risk-score-low { color: #67C23A; font-weight: 700; }
+.foot-score {
+  font-weight: 700;
+  font-variant-numeric: tabular-nums;
+}
 
-.pipe-item-alarm {
-  display: flex;
+.foot-alarm {
+  display: inline-flex;
   align-items: center;
-  gap: 4px;
-  font-size: 12px;
+  gap: 3px;
   color: #F56C6C;
-  margin-top: 4px;
 }
 
-/* 右侧面板 */
+/* ==================== 右侧详情 ==================== */
 .right-panel {
   flex: 1;
   min-width: 0;
+  background: #fff;
+  border-radius: 4px;
+  border: 1px solid #e4e7ed;
+  padding: 16px;
+  overflow: hidden;
 }
 
-/* 报警统计栏 */
-.alarm-stats-bar {
-  display: flex;
-  gap: 24px;
-  padding: 16px 20px;
-  background: #f5f7fa;
-  border-radius: 8px;
-}
-
-.alarm-stat-item {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-}
-
-.alarm-stat-value {
-  font-size: 24px;
-  font-weight: 700;
-  color: #303133;
-}
-
-.alarm-stat-label {
-  font-size: 13px;
-  color: #909399;
-  margin-top: 4px;
-}
-
-/* 报警分页 */
-.alarm-pagination {
-  display: flex;
-  justify-content: flex-end;
-  margin-top: 12px;
+.detail-scroll {
+  height: 100%;
+  overflow-y: auto;
+  min-height: 260px;
 }
 </style>

+ 20 - 28
src/views/subSystem/drainage/jcsj/gcssgl.vue

@@ -41,13 +41,7 @@
     </el-card>
 
     <!-- 查询条件 -->
-    <el-card shadow="never" class="filter-card">
-      <template #header>
-        <div class="card-header">
-          <div class="card-title"><span>查询条件</span></div>
-        </div>
-      </template>
-      <el-form :model="query" :inline="true" label-width="80px" class="filter-form">
+    <el-form ref="queryRef" :model="query" :inline="true" v-show="showSearch" label-width="80px">
         <el-form-item label="设施编码">
           <el-input v-model="query.facilityCode" placeholder="请输入" clearable style="width: 160px"
             @keyup.enter="handleQuery" />
@@ -80,29 +74,27 @@
             @keyup.enter="handleQuery" />
         </el-form-item>
         <el-form-item>
-          <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
-          <el-button :icon="RefreshLeft" @click="resetQuery">重置</el-button>
+          <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+          <el-button icon="Refresh" @click="resetQuery">重置</el-button>
         </el-form-item>
-      </el-form>
-    </el-card>
+    </el-form>
 
     <!-- 设施清单 -->
-    <el-card shadow="never" class="table-card">
-      <template #header>
-        <div class="card-header">
-          <div class="card-title"><span>设施清单</span></div>
-        </div>
-      </template>
-
-      <div class="toolbar">
-        <el-button v-hasPermi="['drainage:facility:add']" type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
-        <el-button v-hasPermi="['drainage:facility:remove']" type="danger" :icon="Delete"
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button v-hasPermi="['drainage:facility:add']" type="primary" plain icon="Plus"
+          @click="handleAdd">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button v-hasPermi="['drainage:facility:remove']" type="danger" plain icon="Delete"
           :disabled="selectedIds.length === 0" @click="handleBatchDelete">
           删除{{ selectedIds.length ? `(${selectedIds.length})` : '' }}
         </el-button>
-      </div>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="loadAll"></right-toolbar>
+    </el-row>
 
-      <el-table v-loading="loading" :data="tableData" border stripe style="width: 100%"
+      <el-table v-loading="loading" :data="tableData" style="width: 100%"
         @selection-change="handleSelectionChange" @row-dblclick="openDetail">
         <el-table-column type="selection" width="50" align="center" />
         <el-table-column label="设施编码" prop="facilityCode" min-width="120" show-overflow-tooltip />
@@ -133,10 +125,8 @@
           </template>
         </el-table-column>
       </el-table>
-      <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total"
-        :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
-        class="pager" @size-change="handleSizeChange" @current-change="loadPage" />
-    </el-card>
+      <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize"
+        @pagination="loadPage" />
 
     <!-- 详情抽屉(只读) -->
     <el-drawer v-model="detailVisible" title="工程设施详情" size="680px" direction="rtl"
@@ -305,7 +295,7 @@
 <script setup name="Gcssgl">
 import { computed, nextTick, onMounted, ref, watch } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { Aim, Delete, Plus, Refresh, RefreshLeft, Search } from '@element-plus/icons-vue'
+import { Aim, Refresh } from '@element-plus/icons-vue'
 import GisMapCanvas from '@/components/GisMapCanvas.vue'
 import { useDict } from '@/utils/dict'
 import { bd09ToWgs84 } from '@/utils/coordTransform'
@@ -349,6 +339,8 @@ const emptyQuery = () => ({
   status: ''
 })
 const query = ref(emptyQuery())
+const showSearch = ref(true)
+const queryRef = ref()
 
 // 设施等级:前端写死可选项,落库存编码(不存中文)
 const levelOptions = [

+ 35 - 17
src/views/subSystem/drainage/jcsj/psfqgl.vue

@@ -11,33 +11,41 @@
     </div>
 
     <!-- 搜索区域 -->
-    <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="80px">
       <el-form-item label="分区名称">
-        <el-input v-model="queryParams.name" placeholder="请输入" clearable style="width: 200px" />
+        <el-input v-model="queryParams.name" placeholder="请输入分区名称" clearable style="width: 200px"
+          @keyup.enter="handleQuery" />
       </el-form-item>
       <el-form-item label="地点">
-        <el-input v-model="queryParams.district" placeholder="请输入" clearable style="width: 200px" />
+        <el-input v-model="queryParams.district" placeholder="请输入地点" clearable style="width: 200px"
+          @keyup.enter="handleQuery" />
       </el-form-item>
       <el-form-item label="分区类型">
-        <el-select v-model="queryParams.partitionType" placeholder="请选择" clearable style="width: 200px">
+        <el-select v-model="queryParams.partitionType" placeholder="请选择分区类型" clearable style="width: 200px">
           <el-option label="雨水分区" value="雨水分区" />
           <el-option label="污水分区" value="污水分区" />
         </el-select>
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" @click="handleQuery">搜索</el-button>
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
       </el-form-item>
     </el-form>
 
     <!-- 操作按钮区域 -->
-    <div class="toolbar">
-      <el-button type="primary" @click="handleAdd">新增</el-button>
-      <el-button type="danger" :disabled="selectedIds.length === 0" @click="handleBatchDelete">删除</el-button>
-    </div>
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="danger" plain icon="Delete" :disabled="selectedIds.length === 0"
+          @click="handleBatchDelete">删除</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="loadData"></right-toolbar>
+    </el-row>
 
     <!-- 数据表格 -->
-    <el-table v-loading="loading" :data="filteredList" border style="width: 100%"
-      @selection-change="handleSelectionChange">
+    <el-table v-loading="loading" :data="filteredList" style="width: 100%" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="分区名称" prop="name" min-width="160" />
       <el-table-column label="地点" prop="district" min-width="100" />
@@ -77,9 +85,8 @@
     </el-table>
 
     <!-- 分页 -->
-    <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total"
-      :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
-      style="margin-top: 16px; justify-content: flex-end;" @size-change="loadData" @current-change="loadData" />
+    <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize"
+      @pagination="loadData" />
 
     <!-- 查看详情对话框 -->
     <el-drawer v-model="dialogVisible" title="详情" size="700px" :before-close="handleClose" @opened="onDialogOpened" class="detail-drawer" direction="rtl">
@@ -267,9 +274,8 @@
         </el-table-column>
       </el-table>
 
-      <el-pagination v-model:current-page="bindPageNum" v-model:page-size="bindPageSize" :total="bindTotal"
-        :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
-        style="margin-top: 16px; justify-content: flex-end;" @size-change="loadBindData" @current-change="loadBindData" />
+      <pagination v-show="bindTotal > 0" v-model:page="bindPageNum" v-model:limit="bindPageSize" :total="bindTotal"
+        @pagination="loadBindData" />
 
       <template #footer>
         <el-button @click="closeBindDialog">取 消</el-button>
@@ -456,6 +462,8 @@ function renderMainMapMarkers() {
 }
 
 // ==================== 搜索参数 ====================
+const showSearch = ref(true)
+const queryRef = ref()
 const queryParams = ref({
   name: '',
   district: '',
@@ -531,6 +539,16 @@ function handleQuery() {
   loadData()
 }
 
+// 重置搜索条件
+function resetQuery() {
+  queryRef.value?.resetFields()
+  queryParams.value.name = ''
+  queryParams.value.district = ''
+  queryParams.value.partitionType = ''
+  pageNum.value = 1
+  loadData()
+}
+
 // ==================== 查看详情对话框 ====================
 const dialogVisible = ref(false)
 const currentRow = ref({})

+ 17 - 24
src/views/subSystem/drainage/jcsj/psgwgl.vue

@@ -40,13 +40,7 @@
     </el-card>
 
     <!-- 查询条件 -->
-    <el-card shadow="never" class="filter-card">
-      <template #header>
-        <div class="card-header">
-          <div class="card-title"><span>查询条件</span></div>
-        </div>
-      </template>
-      <el-form :model="query" :inline="true" label-width="80px" class="filter-form">
+    <el-form ref="queryRef" :model="query" :inline="true" v-show="showSearch" label-width="80px">
         <el-form-item label="设施编码">
           <el-input v-model="query.networkCode" placeholder="请输入" clearable style="width: 160px"
             @keyup.enter="handleQuery" />
@@ -90,21 +84,20 @@
             range-separator="-" start-placeholder="开始" end-placeholder="结束" style="width: 230px" />
         </el-form-item>
         <el-form-item>
-          <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
-          <el-button :icon="RefreshLeft" @click="resetQuery">重置</el-button>
+          <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+          <el-button icon="Refresh" @click="resetQuery">重置</el-button>
         </el-form-item>
-      </el-form>
-    </el-card>
+    </el-form>
 
     <!-- 管网清单 -->
-    <el-card shadow="never" class="table-card">
-      <template #header>
-        <div class="card-header">
-          <div class="card-title"><span>管网清单</span></div>
-        </div>
-      </template>
-      <el-table v-loading="loading" :data="tableData" border stripe style="width: 100%"
-        @row-dblclick="openDetail">
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="loadAll"></right-toolbar>
+    </el-row>
+
+      <el-table v-loading="loading" :data="tableData" style="width: 100%" @row-dblclick="openDetail">
         <el-table-column label="设施编码" prop="networkCode" min-width="120" show-overflow-tooltip />
         <el-table-column label="设施名称" prop="networkName" min-width="150" show-overflow-tooltip />
         <el-table-column label="等级" width="90" align="center">
@@ -131,10 +124,8 @@
           </template>
         </el-table-column>
       </el-table>
-      <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total"
-        :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
-        class="pager" @size-change="handleSizeChange" @current-change="loadPage" />
-    </el-card>
+      <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize"
+        @pagination="loadPage" />
 
     <!-- 详情抽屉(只读) -->
     <el-drawer v-model="detailVisible" title="排水管网详情" size="680px" direction="rtl"
@@ -183,7 +174,7 @@
 <script setup name="Psgwgl">
 import { computed, nextTick, onMounted, ref, watch } from 'vue'
 import { ElMessage } from 'element-plus'
-import { Aim, Refresh, RefreshLeft, Search } from '@element-plus/icons-vue'
+import { Aim, Refresh } from '@element-plus/icons-vue'
 import GisMapCanvas from '@/components/GisMapCanvas.vue'
 import { useDict } from '@/utils/dict'
 import {
@@ -208,6 +199,8 @@ const query = ref({
   diameterMax: null
 })
 const buildRange = ref([])
+const showSearch = ref(true)
+const queryRef = ref()
 
 // 下拉项(材质、管养单位去重值)
 const filterOptions = ref({ materials: [], managementUnits: [] })

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 919 - 512
src/views/subSystem/drainage/jcsj/sbyx.vue


+ 220 - 60
src/views/subSystem/drainage/jcsj/yjsdgl.vue

@@ -33,13 +33,7 @@
     </el-card>
 
     <!-- 查询条件 -->
-    <el-card shadow="never" class="filter-card">
-      <template #header>
-        <div class="card-header">
-          <div class="card-title"><span>查询条件</span></div>
-        </div>
-      </template>
-      <el-form :model="query" :inline="true" label-width="80px" class="filter-form">
+    <el-form ref="queryRef" :model="query" :inline="true" v-show="showSearch" label-width="80px">
         <el-form-item label="点位编码">
           <el-input v-model="query.pointCode" placeholder="请输入" clearable style="width: 160px"
             @keyup.enter="handleQuery" />
@@ -48,9 +42,10 @@
           <el-input v-model="query.pointName" placeholder="请输入" clearable style="width: 160px"
             @keyup.enter="handleQuery" />
         </el-form-item>
-        <el-form-item label="所属区域">
-          <el-input v-model="query.district" placeholder="请输入" clearable style="width: 160px"
-            @keyup.enter="handleQuery" />
+        <el-form-item label="所属片区">
+          <el-select v-model="query.district" placeholder="请选择" clearable style="width: 140px">
+            <el-option v-for="item in districtOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
         </el-form-item>
         <el-form-item label="积水程度">
           <el-select v-model="query.waterloggingLevel" placeholder="请选择" clearable style="width: 140px">
@@ -64,34 +59,34 @@
           </el-select>
         </el-form-item>
         <el-form-item>
-          <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
-          <el-button :icon="RefreshLeft" @click="resetQuery">重置</el-button>
+          <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+          <el-button icon="Refresh" @click="resetQuery">重置</el-button>
         </el-form-item>
-      </el-form>
-    </el-card>
+    </el-form>
 
     <!-- 点位清单 -->
-    <el-card shadow="never" class="table-card">
-      <template #header>
-        <div class="card-header">
-          <div class="card-title"><span>点位清单</span></div>
-        </div>
-      </template>
-
-      <div class="toolbar">
-        <el-button v-hasPermi="['drainage:waterlogging:add']" type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
-        <el-button v-hasPermi="['drainage:waterlogging:remove']" type="danger" :icon="Delete"
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button v-hasPermi="['drainage:waterlogging:add']" type="primary" plain icon="Plus"
+          @click="handleAdd">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button v-hasPermi="['drainage:waterlogging:remove']" type="danger" plain icon="Delete"
           :disabled="selectedIds.length === 0" @click="handleBatchDelete">
           删除{{ selectedIds.length ? `(${selectedIds.length})` : '' }}
         </el-button>
-      </div>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="loadAll"></right-toolbar>
+    </el-row>
 
-      <el-table v-loading="loading" :data="tableData" border stripe style="width: 100%"
+      <el-table v-loading="loading" :data="tableData" style="width: 100%"
         @selection-change="handleSelectionChange" @row-dblclick="openDetail">
         <el-table-column type="selection" width="50" align="center" />
         <el-table-column label="点位编码" prop="pointCode" min-width="110" show-overflow-tooltip />
         <el-table-column label="点位名称" prop="pointName" min-width="140" show-overflow-tooltip />
-        <el-table-column label="所属区域" prop="district" min-width="110" show-overflow-tooltip />
+        <el-table-column label="所属片区" min-width="110" show-overflow-tooltip>
+          <template #default="{ row }">{{ districtLabel(row.district) }}</template>
+        </el-table-column>
         <el-table-column label="位置描述" prop="location" min-width="140" show-overflow-tooltip />
         <el-table-column label="积水程度" width="110" align="center">
           <template #default="{ row }">
@@ -132,10 +127,8 @@
           </template>
         </el-table-column>
       </el-table>
-      <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total"
-        :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
-        class="pager" @size-change="handleSizeChange" @current-change="loadPage" />
-    </el-card>
+      <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize"
+        @pagination="loadPage" />
 
     <!-- 详情抽屉(只读) -->
     <el-drawer v-model="detailVisible" title="易积水点详情" size="700px" direction="rtl"
@@ -143,7 +136,7 @@
       <el-descriptions :column="2" border class="detail-desc">
         <el-descriptions-item label="点位编码">{{ fmt(detailRow.pointCode) }}</el-descriptions-item>
         <el-descriptions-item label="点位名称">{{ fmt(detailRow.pointName) }}</el-descriptions-item>
-        <el-descriptions-item label="所属区域">{{ fmt(detailRow.district) }}</el-descriptions-item>
+        <el-descriptions-item label="所属片区">{{ districtLabel(detailRow.district) }}</el-descriptions-item>
         <el-descriptions-item label="积水程度">
           <el-tag size="small" :type="levelTagType(detailRow.waterloggingLevel)">{{ levelLabel(detailRow.waterloggingLevel) }}</el-tag>
         </el-descriptions-item>
@@ -209,10 +202,9 @@
         </el-row>
         <el-row :gutter="16">
           <el-col :span="12">
-            <el-form-item label="所属区域" prop="district">
-              <el-select v-model="form.district" placeholder="请选择或输入" clearable filterable allow-create
-                style="width: 100%">
-                <el-option v-for="item in filterOptions.district" :key="item" :label="item" :value="item" />
+            <el-form-item label="所属片区" prop="district">
+              <el-select v-model="form.district" placeholder="请选择所属片区" clearable style="width: 100%">
+                <el-option v-for="item in districtOptions" :key="item.value" :label="item.label" :value="item.value" />
               </el-select>
             </el-form-item>
           </el-col>
@@ -261,20 +253,35 @@
             </el-form-item>
           </el-col>
           <el-col :span="12">
-            <el-form-item label="责任单位" prop="dutyUnit">
-              <el-input v-model="form.dutyUnit" placeholder="请输入责任单位" maxlength="200" clearable />
+            <el-form-item label="责任单位" prop="dutyDeptId">
+              <!-- check-strictly:允许选中带下级的部门(默认只有叶子节点可选)
+                   render-after-expand=false:折叠状态的深层部门也参与检索 -->
+              <el-tree-select v-model="form.dutyDeptId" :data="deptTree" clearable filterable check-strictly
+                :render-after-expand="false" :props="{ label: 'label', value: 'id', children: 'children' }"
+                placeholder="请选择或搜索责任单位" style="width: 100%" @change="handleDeptChange" />
+              <div v-if="!form.dutyDeptId && form.dutyUnit" class="legacy-tip">
+                原值:{{ form.dutyUnit }}(未关联系统部门,重选后将覆盖)
+              </div>
             </el-form-item>
           </el-col>
         </el-row>
         <el-row :gutter="16">
           <el-col :span="12">
-            <el-form-item label="责任人" prop="dutyPerson">
-              <el-input v-model="form.dutyPerson" placeholder="请输入责任人" maxlength="64" clearable />
+            <el-form-item label="责任人" prop="dutyUserId">
+              <el-select v-model="form.dutyUserId" filterable clearable :placeholder="dutyUserPlaceholder"
+                :no-data-text="dutyUserEmptyText" style="width: 100%" @change="handleDutyUserChange">
+                <el-option v-for="user in dutyUserOptions" :key="user.userId" :value="user.userId"
+                  :label="`${user.nickName || user.userName}${userDeptName(user) ? '(' + userDeptName(user) + ')' : ''}`" />
+              </el-select>
+              <div v-if="!form.dutyUserId && form.dutyPerson" class="legacy-tip">
+                原值:{{ form.dutyPerson }}(未关联系统用户,重选后将覆盖)
+              </div>
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="联系电话" prop="dutyPhone">
-              <el-input v-model="form.dutyPhone" placeholder="请输入联系电话" maxlength="32" clearable />
+              <el-input v-model="form.dutyPhone" placeholder="选择责任人后自动带出,可手工修改" maxlength="32"
+                clearable />
             </el-form-item>
           </el-col>
         </el-row>
@@ -371,17 +378,17 @@
 <script setup name="Yjsdgl">
 import { computed, nextTick, onMounted, ref, watch } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { Aim, Delete, Plus, Refresh, RefreshLeft, Search } from '@element-plus/icons-vue'
+import { Aim, Refresh } from '@element-plus/icons-vue'
 import GisMapCanvas from '@/components/GisMapCanvas.vue'
 import DrainageLiveTile from '@/views/subSystem/drainage/spjc/DrainageLiveTile.vue'
 import { useDict } from '@/utils/dict'
 import { bd09ToWgs84 } from '@/utils/coordTransform'
 import { getDrainageVideoMap } from '@/api/drainageVideo'
+import { deptTreeSelect, enabledAll } from '@/api/system/user'
 import {
   getWaterloggingPage,
   getWaterloggingMap,
   getWaterloggingDetail,
-  getWaterloggingFilterOptions,
   addWaterlogging,
   updateWaterlogging,
   deleteWaterlogging,
@@ -389,8 +396,21 @@ import {
   bindWaterloggingCameras
 } from '@/api/drainage'
 
-const { drainage_waterlogging_level: levelDict, drainage_waterlogging_risk_level: riskDict } =
-  useDict('drainage_waterlogging_level', 'drainage_waterlogging_risk_level')
+const {
+  drainage_waterlogging_level: levelDict,
+  drainage_waterlogging_risk_level: riskDict,
+  drainage_district: districtDict
+} = useDict('drainage_waterlogging_level', 'drainage_waterlogging_risk_level', 'drainage_district')
+
+/** 所属片区兜底(字典未配置时页面仍可用),与 DistrictEnum 的 0城南/1城北/2太常 对齐 */
+const DISTRICT_FALLBACK = [
+  { value: '0', label: '城南' },
+  { value: '1', label: '城北' },
+  { value: '2', label: '太常' }
+]
+const districtOptions = computed(() =>
+  districtDict.value && districtDict.value.length ? districtDict.value : DISTRICT_FALLBACK
+)
 
 /** 积水程度兜底(字典未配置时页面仍可用) */
 const LEVEL_FALLBACK = [
@@ -421,7 +441,8 @@ const emptyQuery = () => ({
   riskLevel: ''
 })
 const query = ref(emptyQuery())
-const filterOptions = ref({ district: [] })
+const showSearch = ref(true)
+const queryRef = ref()
 
 // ==================== 列表 ====================
 const tableData = ref([])
@@ -618,16 +639,6 @@ async function loadPage() {
   }
 }
 
-async function loadFilterOptions() {
-  try {
-    const res = await getWaterloggingFilterOptions()
-    const data = res && res.code !== undefined ? (res.data || {}) : (res || {})
-    filterOptions.value = { district: data.district || [] }
-  } catch (error) {
-    console.warn('易积水点筛选下拉项加载失败', error)
-  }
-}
-
 function handleQuery() {
   pageNum.value = 1
   loadAll()
@@ -766,7 +777,9 @@ const emptyForm = () => ({
   waterVolume: null,
   waterArea: null,
   riskLevel: '',
+  dutyDeptId: null,
   dutyUnit: '',
+  dutyUserId: null,
   dutyPerson: '',
   dutyPhone: '',
   remark: ''
@@ -779,6 +792,132 @@ const formRules = {
   waterloggingLevel: [{ required: true, message: '请选择积水程度', trigger: 'change' }]
 }
 
+// ==================== 责任单位 / 责任人候选 ====================
+// 取自系统部门与用户:deptTree 用于责任单位下拉,dutyUserList 用于责任人下拉,
+// 选中人员后自动带出姓名、手机号和所属部门,避免各页面各写一套联系方式。
+const deptTree = ref([])
+const dutyUserList = ref([])
+
+/** 部门 id → 名称,供责任单位选中后回填 dutyUnit 快照 */
+const deptNameMap = computed(() => {
+  const map = {}
+  const walk = (nodes) => {
+    ;(nodes || []).forEach((node) => {
+      if (!node) return
+      if (node.id !== undefined && node.id !== null) map[node.id] = node.label
+      if (node.children && node.children.length) walk(node.children)
+    })
+  }
+  walk(deptTree.value)
+  return map
+})
+
+function userDeptName(user) {
+  if (!user) return ''
+  return user.deptName || (user.dept && user.dept.deptName) || ''
+}
+
+/**
+ * 责任单位生效范围:选中部门 + 其全部下级部门
+ *
+ * 人员通常挂在最末级科室上,只按选中节点精确匹配会让上级部门(如「综合办公室」)
+ * 选完就没人可选。所以选父级时把整棵子树都纳入范围,选科室时自然只剩本科室的人。
+ */
+const dutyDeptScope = computed(() => {
+  const scope = new Set()
+  const target = form.value.dutyDeptId
+  if (!target) return scope
+  scope.add(String(target))
+  const walk = (nodes, inScope) => {
+    ;(nodes || []).forEach((node) => {
+      if (!node) return
+      const hit = inScope || String(node.id) === String(target)
+      if (hit) scope.add(String(node.id))
+      if (node.children && node.children.length) walk(node.children, hit)
+    })
+  }
+  walk(deptTree.value, false)
+  return scope
+})
+
+/** 当前责任单位范围内的启用人员(不含兜底快照项) */
+const dutyUserInScope = computed(() => {
+  const scope = dutyDeptScope.value
+  if (!scope.size) return dutyUserList.value
+  return dutyUserList.value.filter((item) => scope.has(String(item.deptId)))
+})
+
+/**
+ * 责任人下拉选项:跟随责任单位过滤,仍支持按姓名/部门名检索(el-select filterable)
+ *
+ * 当前选中的人若已离职/停用,就不在 enabledAll 里了,直接按 ID 渲染会退化成数字;
+ * 这里补一条快照,保证编辑时看得懂。
+ */
+const dutyUserOptions = computed(() => {
+  const list = dutyUserInScope.value
+  const selectedId = form.value.dutyUserId
+  if (!selectedId || list.some((item) => String(item.userId) === String(selectedId))) return list
+  const stale = dutyUserList.value.find((item) => String(item.userId) === String(selectedId))
+  return [
+    ...list,
+    stale || { userId: selectedId, nickName: form.value.dutyPerson || '已停用的用户', deptId: form.value.dutyDeptId }
+  ]
+})
+
+const dutyUserPlaceholder = computed(() =>
+  form.value.dutyDeptId ? '请选择或搜索责任人(限该单位)' : '请选择或搜索责任人'
+)
+
+const dutyUserEmptyText = computed(() =>
+  form.value.dutyDeptId ? '该单位下暂无启用人员,请先调整责任单位' : '暂无可选人员'
+)
+
+function handleDeptChange(deptId) {
+  form.value.dutyUnit = deptId ? (deptNameMap.value[deptId] || '') : ''
+  // 单位变了,原来选的人若不在新范围内,一并清掉,避免单位和责任人错配
+  if (!form.value.dutyUserId) return
+  const stillInScope = dutyUserInScope.value.some(
+    (item) => String(item.userId) === String(form.value.dutyUserId)
+  )
+  if (stillInScope) return
+  form.value.dutyUserId = null
+  form.value.dutyPerson = ''
+  form.value.dutyPhone = ''
+  ElMessage.info('责任单位已变更,请重新选择责任人')
+}
+
+function handleDutyUserChange(userId) {
+  if (!userId) {
+    form.value.dutyPerson = ''
+    form.value.dutyPhone = ''
+    return
+  }
+  const user = dutyUserList.value.find((item) => String(item.userId) === String(userId))
+  if (!user) return
+  form.value.dutyPerson = user.nickName || user.userName || ''
+  form.value.dutyPhone = user.phonenumber || ''
+  // 责任单位还没选时跟随责任人所属部门,省一次手工选择
+  if (!form.value.dutyDeptId && user.deptId) {
+    form.value.dutyDeptId = user.deptId
+    form.value.dutyUnit = userDeptName(user) || deptNameMap.value[user.deptId] || ''
+  }
+}
+
+async function loadDutyResources() {
+  try {
+    const res = await deptTreeSelect()
+    deptTree.value = (res && res.code !== undefined ? res.data : res) || []
+  } catch (error) {
+    console.warn('责任单位(部门树)加载失败', error)
+  }
+  try {
+    const res = await enabledAll()
+    dutyUserList.value = (res && res.code !== undefined ? res.data : res) || []
+  } catch (error) {
+    console.warn('责任人(用户列表)加载失败', error)
+  }
+}
+
 const formPoints = computed(() => {
   const coord = toCoord(form.value.longitude, form.value.latitude)
   if (!coord) return []
@@ -809,7 +948,10 @@ function handleEdit(row) {
     latitude: row.latitude === null || row.latitude === undefined ? null : Number(row.latitude),
     waterDepth: row.waterDepth === null || row.waterDepth === undefined ? null : Number(row.waterDepth),
     waterVolume: row.waterVolume === null || row.waterVolume === undefined ? null : Number(row.waterVolume),
-    waterArea: row.waterArea === null || row.waterArea === undefined ? null : Number(row.waterArea)
+    waterArea: row.waterArea === null || row.waterArea === undefined ? null : Number(row.waterArea),
+    // 部门/人员 ID 是 bigint,下拉的 value 需保持数字类型,否则回显不上
+    dutyDeptId: row.dutyDeptId === null || row.dutyDeptId === undefined ? null : Number(row.dutyDeptId),
+    dutyUserId: row.dutyUserId === null || row.dutyUserId === undefined ? null : Number(row.dutyUserId)
   }
   formVisible.value = true
 }
@@ -847,7 +989,9 @@ function buildPayload() {
   return {
     pointCode: source.pointCode,
     pointName: source.pointName,
-    district: source.district || null,
+    // district 存的是字典编码字符串('0'/'1'/'2'),不能写成 || null:编码 '0' 虽是字符串不受影响,
+    // 但一旦字典值改成数字就会把合法的 0 丢成 null,这里显式判空更稳
+    district: source.district === '' || source.district == null ? null : source.district,
     location: source.location || null,
     longitude: source.longitude,
     latitude: source.latitude,
@@ -856,7 +1000,9 @@ function buildPayload() {
     waterVolume: source.waterVolume,
     waterArea: source.waterArea,
     riskLevel: source.riskLevel || null,
+    dutyDeptId: source.dutyDeptId || null,
     dutyUnit: source.dutyUnit || null,
+    dutyUserId: source.dutyUserId || null,
     dutyPerson: source.dutyPerson || null,
     dutyPhone: source.dutyPhone || null,
     remark: source.remark || null
@@ -875,7 +1021,7 @@ function submitForm() {
       if (res && (res.code === 0 || res.code === 200)) {
         ElMessage.success(editingId.value ? '修改成功' : '新增成功')
         formVisible.value = false
-        await Promise.all([loadAll(), loadFilterOptions()])
+        await loadAll()
       } else {
         ElMessage.error(res?.msg || (editingId.value ? '修改失败' : '新增失败'))
       }
@@ -946,6 +1092,13 @@ function dictItem(dictRef, value) {
   return list.find((item) => String(item.value) === String(value))
 }
 
+function districtLabel(value) {
+  if (value === null || value === undefined || value === '') return '-'
+  const fallback = DISTRICT_FALLBACK.find((item) => item.value === String(value))
+  // 字典与兜底都匹配不上(历史脏数据)时原样显示,避免数据在界面上「消失」
+  return dictItem(districtDict, value)?.label || fallback?.label || String(value)
+}
+
 function levelLabel(value) {
   if (value === null || value === undefined || value === '') return '-'
   const fallback = LEVEL_FALLBACK.find((item) => item.value === String(value))
@@ -992,8 +1145,8 @@ function fmt(value) {
 }
 
 onMounted(() => {
-  loadFilterOptions()
   loadVideoResources()
+  loadDutyResources()
   loadAll()
 })
 </script>
@@ -1181,6 +1334,13 @@ onMounted(() => {
   color: #909399;
 }
 
+/* 历史数据只有手输文本、没有关联到系统部门/用户时的提示 */
+.legacy-tip {
+  font-size: 12px;
+  line-height: 1.4;
+  color: #e6a23c;
+}
+
 .form-map-wrap {
   height: 300px;
   border: 1px solid #e4e7ed;

+ 653 - 0
src/views/subSystem/drainage/jcyj/components/WarningGisDetail.vue

@@ -0,0 +1,653 @@
+<template>
+  <el-drawer v-model="visible" :with-header="false" :close-on-click-modal="false" append-to-body size="780px"
+    class="wgd-drawer" direction="rtl">
+    <div class="wgd" v-if="record" v-loading="loading">
+      <!-- ============ 顶部标题区 ============ -->
+      <div class="wgd-hero" :class="`hero-${levelKey}`">
+        <div class="hero-left">
+          <div class="hero-badge">{{ levelText }}</div>
+          <div class="hero-main">
+            <div class="hero-name" :title="record.equipmentName">{{ record.equipmentName || '未命名预警' }}</div>
+            <div class="hero-sub">
+              <span class="hero-no">{{ record.warningNo || '—' }}</span>
+              <span class="hero-dot">·</span>
+              <span>{{ warningTypeText }}</span>
+            </div>
+          </div>
+        </div>
+        <div class="hero-right">
+          <span class="hero-status" :class="`st-${statusKey}`">{{ statusTextValue }}</span>
+          <el-button class="hero-close" text circle @click="close">
+            <el-icon><Close /></el-icon>
+          </el-button>
+        </div>
+      </div>
+
+      <!-- ============ 关键指标条 ============ -->
+      <div class="wgd-kpi">
+        <div class="kpi-item">
+          <div class="kpi-label">实时液位</div>
+          <div class="kpi-value" :class="{ 'kpi-alarm': alarm }">
+            <template v-if="metric.level !== null">{{ metric.level }}<i>m</i></template>
+            <span v-else class="kpi-empty">—</span>
+          </div>
+        </div>
+        <div class="kpi-item">
+          <div class="kpi-label">报警阈值</div>
+          <div class="kpi-value kpi-text">{{ metric.thresholdText || '未配置' }}</div>
+        </div>
+        <div class="kpi-item">
+          <div class="kpi-label">采集时间</div>
+          <div class="kpi-value kpi-text">{{ metric.collectTime || '—' }}</div>
+        </div>
+        <div class="kpi-item">
+          <div class="kpi-label">{{ isHistory ? '归档时间' : '预警时间' }}</div>
+          <div class="kpi-value kpi-text">
+            {{ formatDateTime(isHistory ? archiveTime : record.createTime) }}
+          </div>
+        </div>
+      </div>
+
+      <!-- ============ 预警信息 ============ -->
+      <div class="wgd-section">
+        <div class="section-head">
+          <span class="section-bar"></span>
+          <span class="section-title">预警信息</span>
+        </div>
+        <el-descriptions :column="2" border size="small">
+          <el-descriptions-item label="预警编号">{{ record.warningNo || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="预警名称">{{ record.equipmentName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="预警类型">{{ warningTypeText }}</el-descriptions-item>
+          <el-descriptions-item label="预警级别">
+            <el-tag :type="levelTagType" size="small">{{ levelText }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="预警专项">{{ record.warningSpecial || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="权属单位">{{ record.ownershipUnit || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="预警时间">{{ formatDateTime(record.createTime) }}</el-descriptions-item>
+          <el-descriptions-item :label="isHistory ? '处理状态' : '当前状态'">
+            <el-tag :type="statusTagType" size="small">{{ statusTextValue }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="位置" :span="2">{{ record.location || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="预警内容" :span="2">
+            <div class="desc-text">{{ record.content || '-' }}</div>
+          </el-descriptions-item>
+          <el-descriptions-item v-if="record.remark" label="备注" :span="2">{{ record.remark }}</el-descriptions-item>
+        </el-descriptions>
+      </div>
+
+      <!-- ============ 归档信息(仅历史预警) ============ -->
+      <div class="wgd-section" v-if="isHistory">
+        <div class="section-head">
+          <span class="section-bar bar-green"></span>
+          <span class="section-title">归档信息</span>
+        </div>
+        <el-descriptions :column="2" border size="small">
+          <el-descriptions-item label="归档编号">{{ archiveNo }}</el-descriptions-item>
+          <el-descriptions-item label="归档时间">{{ formatDateTime(archiveTime) }}</el-descriptions-item>
+          <el-descriptions-item label="发布时间">{{ formatDateTime(record.publishTime) }}</el-descriptions-item>
+          <el-descriptions-item label="处置人">{{ record.processor || '-' }}</el-descriptions-item>
+        </el-descriptions>
+      </div>
+
+      <!-- ============ 关联设备与预警指标 ============ -->
+      <div class="wgd-section">
+        <div class="section-head">
+          <span class="section-bar bar-blue"></span>
+          <span class="section-title">关联设备与预警指标</span>
+        </div>
+        <el-descriptions v-if="device" :column="2" border size="small">
+          <el-descriptions-item label="设备名称">{{ device.equipmentName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="设备编码">{{ device.equipmentCode || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="设备类型">
+            {{ equipmentTypeMap.get(String(device.equipmentTypeId)) || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="在线状态">
+            <el-tag :type="device.onlineStatus === 1 ? 'success' : 'info'" size="small">
+              {{ device.onlineStatus === 1 ? '在线' : '离线' }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="安装位置" :span="2">
+            {{ device.equipmentLocation || record.location || '-' }}
+          </el-descriptions-item>
+        </el-descriptions>
+        <el-alert v-else type="info" :closable="false" show-icon class="device-missing"
+          title="未在设备台账中找到该预警关联的设备,实时指标无法展示" />
+
+        <div class="threshold-table" v-if="thresholds.length">
+          <el-table :data="thresholds" border size="small">
+            <el-table-column label="监测指标" prop="warningName" min-width="140" />
+            <el-table-column label="单位" width="90" align="center">
+              <template #default="{ row }">{{ row.warningUnit || '-' }}</template>
+            </el-table-column>
+            <el-table-column label="阈值范围" min-width="160" align="center">
+              <template #default="{ row }">{{ formatThreshold(row) }}</template>
+            </el-table-column>
+          </el-table>
+        </div>
+        <div class="threshold-empty" v-else-if="device">该设备尚未配置报警阈值</div>
+      </div>
+
+      <!-- ============ 处置信息 ============ -->
+      <div class="wgd-section">
+        <div class="section-head">
+          <span class="section-bar bar-orange"></span>
+          <span class="section-title">处置信息</span>
+        </div>
+        <el-descriptions :column="2" border size="small">
+          <el-descriptions-item label="发布人">{{ record.publisher || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="创建人">{{ record.createBy || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="处置人">{{ record.processor || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="最后更新">{{ formatDateTime(record.updateTime) }}</el-descriptions-item>
+        </el-descriptions>
+      </div>
+
+      <!-- ============ 底部 ============ -->
+      <div class="wgd-footer">
+        <el-button @click="close">关 闭</el-button>
+      </div>
+    </div>
+  </el-drawer>
+</template>
+
+<script setup name="WarningGisDetail">
+import { ref, computed, watch } from 'vue'
+import { Close } from '@element-plus/icons-vue'
+import { getEquipmentTypeList, getLatestWaterLevelByDevice } from '@/api/drainage'
+import { listEquipment } from '@/api/pipeNetwork/pdeviceInform'
+import { pageWarningThresholds } from '@/api/warningThreshold'
+import { listWarningTypes } from '@/api/warning/list'
+
+const props = defineProps({
+  modelValue: { type: Boolean, default: false },
+  /** 预警原始记录(保留后端返回的全部字段,展示靠它) */
+  record: { type: Object, default: null },
+  /** current=当前预警,history=历史预警(多一块归档信息) */
+  mode: { type: String, default: 'current' }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+})
+
+const isHistory = computed(() => props.mode === 'history')
+
+// ==================== 基础字典 / 映射(模块级缓存,两个 GIS 页共用一次请求)====================
+const equipmentTypeMap = ref(new Map())
+const warningTypeMap = ref(new Map())
+let dictPromise = null
+
+function loadDicts() {
+  if (dictPromise) return dictPromise
+  dictPromise = Promise.allSettled([
+    getEquipmentTypeList('2'),
+    listWarningTypes()
+  ]).then(([typeResult, warnResult]) => {
+    if (typeResult.status === 'fulfilled') {
+      const res = typeResult.value
+      const list = res?.code !== undefined ? res.data : res
+      const map = new Map()
+      if (Array.isArray(list)) {
+        list.forEach((item) => {
+          if (item?.id !== undefined && item?.id !== null) map.set(String(item.id), item.typeName)
+        })
+      }
+      equipmentTypeMap.value = map
+    }
+    if (warnResult.status === 'fulfilled') {
+      const res = warnResult.value
+      const list = res?.data || res?.rows || []
+      const map = new Map()
+      if (Array.isArray(list)) {
+        list.forEach((item) => {
+          if (item?.dictValue) map.set(String(item.dictValue), item.dictLabel || item.dictValue)
+        })
+      }
+      warningTypeMap.value = map
+    }
+  })
+  return dictPromise
+}
+
+// ==================== 设备 / 实时指标(模块级缓存)====================
+const deviceByName = ref(null)
+const waterLevelMap = ref({})
+const thresholdMap = ref({})
+let metricPromise = null
+
+const device = ref(null)
+const thresholds = ref([])
+const loading = ref(false)
+
+/** 按设备名称反查排水设备台账(一页 100 条,最多 10 页) */
+async function resolveDevice(equipmentName) {
+  if (!equipmentName) return null
+  if (!deviceByName.value) {
+    const map = new Map()
+    let pageNum = 1
+    while (pageNum <= 10) {
+      const res = await listEquipment({ rootTypeId: 2, pageNum, pageSize: 100 })
+      const page = res?.data && Array.isArray(res.data.records) ? res.data : res
+      const rows = page?.records || page?.rows || []
+      if (!rows.length) break
+      rows.forEach((item) => {
+        if (item?.equipmentName && !map.has(item.equipmentName)) map.set(item.equipmentName, item)
+      })
+      if (rows.length < 100) break
+      pageNum += 1
+    }
+    deviceByName.value = map
+  }
+  return deviceByName.value.get(equipmentName) || null
+}
+
+/** 实时液位 + 报警阈值,按设备编码索引 */
+function loadLiveMetrics() {
+  if (metricPromise) return metricPromise
+  metricPromise = Promise.allSettled([
+    getLatestWaterLevelByDevice(),
+    pageWarningThresholds({ rootTypeId: 2, pageNum: 1, pageSize: 500 })
+  ]).then(([levelResult, thresholdResult]) => {
+    if (levelResult.status === 'fulfilled') {
+      const value = levelResult.value
+      const list = Array.isArray(value) ? value : (value?.data || [])
+      const map = {}
+      list.forEach((item) => {
+        if (!item?.deviceCode) return
+        map[item.deviceCode] = { value: item.waterLevel, time: item.createTime }
+      })
+      waterLevelMap.value = map
+    }
+    if (thresholdResult.status === 'fulfilled') {
+      const res = thresholdResult.value
+      const page = res?.data && Array.isArray(res.data.records) ? res.data : res
+      const rows = page?.records || page?.rows || []
+      const map = {}
+      rows.forEach((row) => {
+        if (!row?.deviceCode) return
+        if (!map[row.deviceCode]) map[row.deviceCode] = []
+        map[row.deviceCode].push(row)
+      })
+      thresholdMap.value = map
+    }
+  })
+  return metricPromise
+}
+
+// ==================== 展示计算 ====================
+const levelKey = computed(() => {
+  const map = { 1: 'severe', 2: 'important', 3: 'normal' }
+  return map[props.record?.warningLevel] || 'normal'
+})
+const levelText = computed(() => {
+  const map = { 1: '严重', 2: '重要', 3: '一般' }
+  return map[props.record?.warningLevel] || '一般'
+})
+const levelTagType = computed(() => {
+  const map = { severe: 'danger', important: 'warning', normal: '' }
+  return map[levelKey.value] || 'info'
+})
+
+const statusKey = computed(() => String(props.record?.status || 'unknown').toLowerCase())
+const statusTextValue = computed(() => {
+  const map = {
+    DRAFT: '草稿', PENDING: '待办', RECEIVED: '已接收',
+    PROCESSING: '处置中', HANDLED: '已处置', CLOSED: '已解除'
+  }
+  return map[props.record?.status] || '-'
+})
+const statusTagType = computed(() => {
+  const map = {
+    DRAFT: 'info', PENDING: 'warning', RECEIVED: 'warning',
+    PROCESSING: 'warning', HANDLED: 'success', CLOSED: ''
+  }
+  return map[props.record?.status] || 'info'
+})
+
+/** 预警类型:新数据看 warningType,历史数据回退到 deviceCode 的设备类型名 */
+const warningTypeText = computed(() => {
+  const row = props.record
+  if (!row) return '-'
+  const byType = row.warningType ? warningTypeMap.value.get(String(row.warningType)) : null
+  if (byType) return byType
+  const byDevice = row.deviceCode ? equipmentTypeMap.value.get(String(row.deviceCode)) : null
+  if (byDevice) return byDevice
+  return row.warningType || '未分类'
+})
+
+/** 归档编号:预警解除即归档,由预警编号派生 */
+const archiveNo = computed(() => {
+  const no = props.record?.warningNo
+  if (!no) return '-'
+  return `ARC${String(no).replace(/^WRN/, '').slice(-12)}`
+})
+const archiveTime = computed(() => props.record?.updateTime || props.record?.publishTime || null)
+
+/** 实时指标 + 是否越限 */
+const metric = computed(() => {
+  const code = device.value?.equipmentCode
+  const levelItem = code ? waterLevelMap.value[code] : null
+  const list = code ? (thresholdMap.value[code] || []) : []
+  const value = levelItem && levelItem.value !== undefined && levelItem.value !== null ? levelItem.value : null
+  return {
+    level: value,
+    collectTime: levelItem?.time ? formatDateTime(levelItem.time) : '',
+    thresholdText: list.length ? list.map(formatThreshold).join(' / ') : '',
+    max: pickUpper(list)
+  }
+})
+
+const alarm = computed(() => {
+  const level = metric.value.level
+  const max = metric.value.max
+  if (level === null || max === null) return false
+  return Number(level) > Number(max)
+})
+
+/** 取阈值里最严的上限(区间取 maxValue,上限型取 maxValue) */
+function pickUpper(list) {
+  let upper = null
+  list.forEach((item) => {
+    if (item?.dataType === 1) return
+    const val = item?.maxValue
+    if (val === null || val === undefined || val === '') return
+    const num = Number(val)
+    if (Number.isNaN(num)) return
+    upper = upper === null ? num : Math.min(upper, num)
+  })
+  return upper
+}
+
+function formatThreshold(item) {
+  if (!item) return '-'
+  const unit = item.warningUnit || ''
+  const min = item.minValue
+  const max = item.maxValue
+  if (item.dataType === 1) return min === null || min === undefined ? '-' : `≥ ${min}${unit}`
+  if (item.dataType === 2) return max === null || max === undefined ? '-' : `≤ ${max}${unit}`
+  if ((min === null || min === undefined) && (max === null || max === undefined)) return '未设置'
+  return `${min ?? '-'} ~ ${max ?? '-'}${unit}`
+}
+
+function formatDateTime(value) {
+  if (!value) return '-'
+  return String(value).replace('T', ' ').slice(0, 19)
+}
+
+// ==================== 加载 / 关闭 ====================
+async function loadDetail(row) {
+  device.value = null
+  thresholds.value = []
+  if (!row) return
+  loading.value = true
+  try {
+    await loadDicts()
+    const [matched] = await Promise.all([resolveDevice(row.equipmentName), loadLiveMetrics()])
+    device.value = matched || null
+    const code = matched?.equipmentCode
+    thresholds.value = code ? (thresholdMap.value[code] || []) : []
+  } catch (error) {
+    console.warn('预警详情加载失败', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+function close() {
+  visible.value = false
+}
+
+watch(
+  () => [props.modelValue, props.record],
+  ([shown, row]) => {
+    if (shown) loadDetail(row)
+  },
+  { immediate: true }
+)
+</script>
+
+<style scoped>
+.wgd {
+  padding: 0 4px 8px;
+}
+
+/* ==================== 顶部标题区 ==================== */
+.wgd-hero {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  margin: -20px -20px 0;
+  padding: 22px 20px 20px;
+  color: #fff;
+  background: linear-gradient(135deg, #3a4a63 0%, #22304a 100%);
+}
+
+.wgd-hero.hero-severe {
+  background: linear-gradient(135deg, #d9534f 0%, #8e2b28 100%);
+}
+
+.wgd-hero.hero-important {
+  background: linear-gradient(135deg, #e6a23c 0%, #a86f16 100%);
+}
+
+.wgd-hero.hero-normal {
+  background: linear-gradient(135deg, #409eff 0%, #1f5fa8 100%);
+}
+
+.hero-left {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  min-width: 0;
+}
+
+.hero-badge {
+  flex-shrink: 0;
+  width: 54px;
+  height: 54px;
+  border-radius: 10px;
+  background: rgba(255, 255, 255, 0.18);
+  border: 1px solid rgba(255, 255, 255, 0.35);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 17px;
+  font-weight: 700;
+  letter-spacing: 1px;
+}
+
+.hero-main {
+  min-width: 0;
+}
+
+.hero-name {
+  font-size: 18px;
+  font-weight: 600;
+  color: #fff;
+  line-height: 1.3;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  max-width: 460px;
+}
+
+.hero-sub {
+  margin-top: 6px;
+  font-size: 12px;
+  color: rgba(255, 255, 255, 0.78);
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.hero-no {
+  font-family: Consolas, Monaco, monospace;
+  letter-spacing: 0.5px;
+}
+
+.hero-dot {
+  opacity: 0.6;
+}
+
+.hero-right {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-shrink: 0;
+}
+
+.hero-status {
+  padding: 3px 10px;
+  border-radius: 12px;
+  font-size: 12px;
+  background: rgba(255, 255, 255, 0.2);
+  border: 1px solid rgba(255, 255, 255, 0.35);
+}
+
+.hero-status.st-pending,
+.hero-status.st-received,
+.hero-status.st-processing {
+  background: rgba(255, 255, 255, 0.28);
+}
+
+.hero-close {
+  color: rgba(255, 255, 255, 0.85);
+}
+
+.hero-close:hover {
+  color: #fff;
+  background: rgba(255, 255, 255, 0.16);
+}
+
+/* ==================== 关键指标条 ==================== */
+.wgd-kpi {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 1px;
+  background: #ebeef5;
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  overflow: hidden;
+  margin: 16px 0;
+}
+
+.kpi-item {
+  background: #fafbfc;
+  padding: 12px 14px;
+  min-width: 0;
+}
+
+.kpi-label {
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 6px;
+}
+
+.kpi-value {
+  font-size: 22px;
+  font-weight: 600;
+  color: #303133;
+  line-height: 1.2;
+}
+
+.kpi-value i {
+  font-style: normal;
+  font-size: 12px;
+  font-weight: 400;
+  color: #909399;
+  margin-left: 2px;
+}
+
+.kpi-value.kpi-text {
+  font-size: 13px;
+  font-weight: 500;
+  word-break: break-all;
+  line-height: 1.5;
+}
+
+.kpi-value.kpi-alarm {
+  color: #f56c6c;
+}
+
+.kpi-empty {
+  font-size: 20px;
+  color: #c0c4cc;
+}
+
+/* ==================== 分区 ==================== */
+.wgd-section {
+  margin-bottom: 18px;
+}
+
+.section-head {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 10px;
+}
+
+.section-bar {
+  width: 3px;
+  height: 14px;
+  border-radius: 2px;
+  background: #f56c6c;
+}
+
+.section-bar.bar-green {
+  background: #67c23a;
+}
+
+.section-bar.bar-blue {
+  background: #409eff;
+}
+
+.section-bar.bar-orange {
+  background: #e6a23c;
+}
+
+.section-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.desc-text {
+  background: #f5f7fa;
+  border-radius: 4px;
+  padding: 10px;
+  font-size: 13px;
+  color: #606266;
+  line-height: 1.6;
+  max-height: 130px;
+  overflow-y: auto;
+  white-space: pre-wrap;
+  word-break: break-all;
+}
+
+.device-missing {
+  margin-bottom: 10px;
+}
+
+.threshold-table {
+  margin-top: 12px;
+}
+
+.threshold-empty {
+  margin-top: 10px;
+  font-size: 12px;
+  color: #909399;
+  background: #f5f7fa;
+  border-radius: 4px;
+  padding: 8px 12px;
+}
+
+/* ==================== 底部 ==================== */
+.wgd-footer {
+  display: flex;
+  justify-content: flex-end;
+  padding-top: 4px;
+}
+</style>

+ 349 - 361
src/views/subSystem/drainage/jcyj/dqyjGIS.vue

@@ -1,155 +1,136 @@
 <template>
   <div class="gis-page">
-    <!-- 左侧浮动面板 -->
-    <div class="side-panel">
+    <!-- ==================== 左侧浮动面板 ==================== -->
+    <aside class="side-panel">
       <div class="panel-header">
-        <span class="panel-title">当前预警分布</span>
-      </div>
-
-      <!-- 等级筛选 -->
-      <div class="panel-section">
-        <div class="section-label">等级筛选</div>
-        <div class="layer-list">
-          <div class="layer-item">
-            <el-checkbox v-model="levelChecked.severe" @change="onLevelChange">
-              <span class="level-dot" style="background:#f56c6c"></span> 严重
-            </el-checkbox>
-          </div>
-          <div class="layer-item">
-            <el-checkbox v-model="levelChecked.important" @change="onLevelChange">
-              <span class="level-dot" style="background:#e6a23c"></span> 重要
-            </el-checkbox>
-          </div>
-          <div class="layer-item">
-            <el-checkbox v-model="levelChecked.normal" @change="onLevelChange">
-              <span class="level-dot" style="background:#409eff"></span> 一般
-            </el-checkbox>
-          </div>
+        <div class="panel-title">当前预警分布</div>
+        <div class="panel-sub">
+          共 <b>{{ warningList.length }}</b> 条 · 已上图 <b>{{ mapPoints.length }}</b> 条
         </div>
       </div>
 
-      <!-- 统计数据 -->
+      <!-- 概览(点击卡片按等级筛选) -->
       <div class="panel-section">
-        <div class="section-label">预警统计</div>
-        <div class="stats-row">
-          <div class="stat-card">
-            <div class="stat-value">{{ warningList.length }}</div>
-            <div class="stat-text">总数</div>
+        <div class="section-label">
+          预警概览
+          <span class="section-hint">点击卡片筛选等级</span>
+        </div>
+        <div class="stat-grid">
+          <div class="stat-card" :class="{ active: levelFilter === 'all' }" @click="setLevel('all')">
+            <div class="stat-value">{{ rawList.length }}</div>
+            <div class="stat-text">全部</div>
           </div>
-          <div class="stat-card stat-severe">
+          <div class="stat-card stat-severe" :class="{ active: levelFilter === 'severe' }" @click="setLevel('severe')">
             <div class="stat-value">{{ severeCount }}</div>
             <div class="stat-text">严重</div>
           </div>
-          <div class="stat-card stat-important">
+          <div class="stat-card stat-important" :class="{ active: levelFilter === 'important' }"
+            @click="setLevel('important')">
             <div class="stat-value">{{ importantCount }}</div>
             <div class="stat-text">重要</div>
           </div>
-          <div class="stat-card stat-normal">
+          <div class="stat-card stat-normal" :class="{ active: levelFilter === 'normal' }" @click="setLevel('normal')">
             <div class="stat-value">{{ normalCount }}</div>
             <div class="stat-text">一般</div>
           </div>
         </div>
       </div>
 
-      <!-- 搜索框 -->
+      <!-- 预警类型筛选 -->
       <div class="panel-section">
-        <div class="section-label">快速定位</div>
-        <el-input
-          v-model="searchKey"
-          placeholder="输入设备名称搜索"
-          clearable
-          prefix-icon="Search"
-          @input="onSearch"
-        />
-        <div class="search-results" v-if="searchResults.length > 0">
-          <div
-            class="search-item"
-            v-for="item in searchResults"
-            :key="item.id"
-            @click="locatePoint(item)"
-          >
-            <span class="level-dot" :style="{ background: getLevelColor(item.level) }"></span>
-            <span class="search-name">{{ item.name }}</span>
-            <span class="search-level" :class="getLevelClass(item.level)">{{ item.level }}</span>
-          </div>
+        <div class="section-label">
+          预警类型
+          <span class="link-btn" v-if="typeFilter || levelFilter !== 'all'" @click="resetFilter">重置</span>
         </div>
+        <el-select v-model="typeFilter" placeholder="全部类型" clearable filterable size="small" style="width:100%">
+          <el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value" />
+        </el-select>
       </div>
-    </div>
 
-    <!-- 地图容器 -->
-    <div id="dqyjGISMap" class="map-container"></div>
-
-    <!-- 详情弹窗 -->
-    <el-dialog
-      v-model="detailVisible"
-      :title="currentItem.name"
-      width="480px"
-      :close-on-click-modal="false"
-      class="detail-dialog"
-    >
-      <div class="detail-content">
-        <div class="detail-header">
-          <el-tag :type="getTagType(currentItem.level)" effect="dark" size="small">
-            {{ currentItem.level }}
-          </el-tag>
-          <span class="detail-name">{{ currentItem.name }}</span>
+      <!-- 预警清单 -->
+      <div class="panel-section list-section">
+        <div class="section-label">
+          预警清单
+          <span class="section-hint">{{ displayList.length }} 条</span>
         </div>
-        <div class="detail-info">
-          <div class="info-row">
-            <span class="info-label">预警编号</span>
-            <span class="info-value">{{ currentItem.warningNo }}</span>
-          </div>
-          <div class="info-row">
-            <span class="info-label">预警级别</span>
-            <span class="info-value">
-              <el-tag :type="getTagType(currentItem.level)" effect="dark" size="small">{{ currentItem.level }}</el-tag>
-            </span>
-          </div>
-          <div class="info-row">
-            <span class="info-label">预警时间</span>
-            <span class="info-value">{{ currentItem.createTime }}</span>
-          </div>
-          <div class="info-row">
-            <span class="info-label">位置</span>
-            <span class="info-value">{{ currentItem.location }}</span>
-          </div>
-          <div class="info-row">
-            <span class="info-label">状态</span>
-            <span class="info-value">{{ currentItem.statusText }}</span>
+        <el-input v-model="searchKey" placeholder="搜索名称 / 位置" clearable size="small" :prefix-icon="Search" />
+        <div class="warning-list">
+          <div class="warning-item" v-for="item in displayList" :key="item.id"
+            :class="{ 'no-coord': !item.lng }" @click="locate(item)">
+            <span class="level-dot" :style="{ background: getLevelColor(item.level) }"></span>
+            <div class="wi-main">
+              <div class="wi-name" :title="item.name">{{ item.name }}</div>
+              <div class="wi-sub">{{ item.typeText }} · {{ formatTime(item.createTime) }}</div>
+            </div>
+            <span class="wi-level" :class="getLevelClass(item.level)">{{ item.level }}</span>
           </div>
+          <div class="list-empty" v-if="!displayList.length">暂无符合条件的预警</div>
         </div>
       </div>
-    </el-dialog>
+    </aside>
+
+    <!-- ==================== 地图容器 ==================== -->
+    <div id="dqyjGISMap" class="map-container"></div>
+
+    <!-- 地图工具条 -->
+    <div class="map-tools">
+      <el-tooltip content="回到预警范围" placement="left">
+        <el-button circle @click="fitView"><el-icon><Aim /></el-icon></el-button>
+      </el-tooltip>
+    </div>
+
+    <!-- ==================== 详情抽屉 ==================== -->
+    <WarningGisDetail v-model="detailVisible" :record="currentRecord" mode="current" />
   </div>
 </template>
 
 <script setup name="DqyjGIS">
-import { ref, reactive, computed, onMounted, nextTick } from 'vue'
-import { Search } from '@element-plus/icons-vue'
+import { ref, computed, watch, onMounted, nextTick } from 'vue'
+import { Search, Aim } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 import locationIcon from '@/assets/images/location.png'
 import { getWarningCurrentPage } from '@/api/drainage'
+import { listWarningTypes } from '@/api/warning/list'
 import useUserStore from '@/store/modules/user'
+import WarningGisDetail from './components/WarningGisDetail.vue'
 
 // ==================== 用户信息 ====================
 const userStore = useUserStore()
 const isAdmin = computed(() => userStore.roles && userStore.roles.includes('admin'))
 
-// ==================== 等级筛选 ====================
-const levelChecked = reactive({ severe: true, important: true, normal: true })
-
-// ==================== 预警数据(从API加载)====================
+// ==================== 数据 ====================
+/** 后端原始记录,详情抽屉直接用(保留全部字段) */
+const rawList = ref([])
+/** 归一化后的全部预警(含无坐标的,清单里照样能看详情) */
 const warningList = ref([])
 
-// 预警级别数字→文字映射
+/** 能上图的点位:百度的 Point 只接受有效经纬度 */
+const mapPoints = computed(() => warningList.value.filter((p) => p.lng && p.lat))
+
 const levelMap = { 1: '严重', 2: '重要', 3: '一般' }
+const levelKeyMap = { 1: 'severe', 2: 'important', 3: 'normal' }
+const statusMap = { DRAFT: '草稿', PENDING: '待办', RECEIVED: '已接收', PROCESSING: '处置中', HANDLED: '已处置', CLOSED: '已解除' }
 
-function statusText(status) {
-  const map = { DRAFT: '草稿', PENDING: '待办', RECEIVED: '已接收', PROCESSING: '处置中', HANDLED: '已处置', CLOSED: '已解除' }
-  return map[status] ?? '-'
+/** 预警类型下拉:warningType 去重,名称取 warning_type 配置 */
+const typeOptions = ref([])
+const warningTypeMap = ref(new Map())
+
+async function loadWarningTypes() {
+  try {
+    const res = await listWarningTypes()
+    const list = res?.data || res?.rows || []
+    const map = new Map()
+    if (Array.isArray(list)) {
+      list.forEach((item) => {
+        if (item?.dictValue) map.set(String(item.dictValue), item.dictLabel || item.dictValue)
+      })
+    }
+    warningTypeMap.value = map
+  } catch (error) {
+    console.warn('加载预警类型配置失败', error)
+  }
 }
 
-// 加载当前预警数据
 async function loadData() {
   try {
     const params = {
@@ -158,43 +139,89 @@ async function loadData() {
       userId: userStore.id,
       isAdmin: isAdmin.value
     }
-    if (userStore.deptId) {
-      params.deptId = userStore.deptId
-    }
-    const res = await getWarningCurrentPage(1, 1000, params)
+    if (userStore.deptId) params.deptId = userStore.deptId
+
+    const [res] = await Promise.all([getWarningCurrentPage(1, 1000, params), loadWarningTypes()])
     const pageData = res.code !== undefined ? res.data : res
     const records = pageData.records || []
-    const hasCoordCount = records.filter(w => w.longitude && w.latitude).length
-    console.log('[GIS预警] API返回记录数:', records.length, '有坐标记录数:', hasCoordCount)
-    if (records.length > 0 && hasCoordCount === 0) {
-      console.warn('[GIS预警] 所有预警均无经纬度坐标,请检查发布预警时设备是否有定位数据')
-    }
-    const noCoordRecords = records.filter(w => !w.longitude || !w.latitude)
-    if (noCoordRecords.length > 0) {
-      console.warn('[GIS预警] 无坐标预警(将被过滤):', noCoordRecords.map(w => ({ id: w.id, name: w.equipmentName, longitude: w.longitude, latitude: w.latitude })))
+    rawList.value = records
+
+    warningList.value = records.map(normalize)
+
+    // 类型下拉:按记录里实际出现的类型构建
+    const types = [...new Set(records.map((r) => r.warningType).filter(Boolean))]
+    typeOptions.value = types.map((val) => ({
+      value: String(val),
+      label: warningTypeMap.value.get(String(val)) || String(val)
+    }))
+
+    const noCoord = records.length - warningList.value.filter((p) => p.lng && p.lat).length
+    if (noCoord > 0) {
+      console.warn(`[GIS预警] ${noCoord} 条预警缺少经纬度,未上图(清单中仍可查看详情)`)
     }
-    warningList.value = records.map(w => ({
-      id: w.id,
-      name: w.equipmentName || '-',
-      warningNo: w.warningNo || '-',
-      deviceCode: w.deviceCode || w.warningType || '',
-      level: levelMap[w.warningLevel] || '一般',
-      createTime: w.createTime || '-',
-      location: w.location || '-',
-      statusText: statusText(w.status),
-      lng: parseFloat(w.longitude),
-      lat: parseFloat(w.latitude)
-    })).filter(w => w.lng && w.lat)
   } catch (error) {
     console.error('加载当前预警数据失败', error)
     ElMessage.error('加载当前预警数据失败')
   }
 }
 
-// ==================== 统计计算 ====================
-const severeCount = computed(() => warningList.value.filter(v => v.level === '严重').length)
-const importantCount = computed(() => warningList.value.filter(v => v.level === '重要').length)
-const normalCount = computed(() => warningList.value.filter(v => v.level === '一般').length)
+function normalize(w) {
+  return {
+    id: w.id,
+    name: w.equipmentName || '-',
+    level: levelMap[w.warningLevel] || '一般',
+    levelKey: levelKeyMap[w.warningLevel] || 'normal',
+    typeValue: w.warningType ? String(w.warningType) : '',
+    typeText: w.warningType ? (warningTypeMap.value.get(String(w.warningType)) || String(w.warningType)) : '未分类',
+    createTime: w.createTime || '',
+    location: w.location || '',
+    statusText: statusMap[w.status] || '-',
+    raw: w,
+    lng: parseFloat(w.longitude),
+    lat: parseFloat(w.latitude)
+  }
+}
+
+// ==================== 统计 ====================
+const severeCount = computed(() => rawList.value.filter((v) => v.warningLevel === 1).length)
+const importantCount = computed(() => rawList.value.filter((v) => v.warningLevel === 2).length)
+const normalCount = computed(() => rawList.value.filter((v) => v.warningLevel === 3).length)
+
+// ==================== 筛选 ====================
+const levelFilter = ref('all')
+const typeFilter = ref('')
+const searchKey = ref('')
+
+const displayList = computed(() => {
+  const key = searchKey.value.trim().toLowerCase()
+  return warningList.value.filter((item) => {
+    if (levelFilter.value !== 'all' && item.levelKey !== levelFilter.value) return false
+    if (typeFilter.value && item.typeValue !== typeFilter.value) return false
+    if (key && !(item.name.toLowerCase().includes(key) || item.location.toLowerCase().includes(key))) return false
+    return true
+  })
+})
+
+function setLevel(key) {
+  levelFilter.value = levelFilter.value === key ? 'all' : key
+}
+
+function resetFilter() {
+  levelFilter.value = 'all'
+  typeFilter.value = ''
+  searchKey.value = ''
+}
+
+// 筛选变化时同步 marker 显隐
+watch(displayList, () => {
+  const visibleIds = new Set(displayList.value.map((item) => item.id))
+  warningList.value.forEach((point) => {
+    const label = markerMap.get(point.id)
+    if (!label) return
+    if (visibleIds.has(point.id)) label.show()
+    else label.hide()
+  })
+})
 
 // ==================== 工具方法 ====================
 function getLevelColor(level) {
@@ -207,41 +234,12 @@ function getLevelClass(level) {
   return map[level] || ''
 }
 
-function getTagType(level) {
-  const map = { '严重': 'danger', '重要': 'warning', '一般': '' }
-  return map[level] || ''
-}
-
-function getLevelKey(level) {
-  const map = { '严重': 'severe', '重要': 'important', '一般': 'normal' }
-  return map[level] || 'normal'
-}
-
-// ==================== 搜索功能 ====================
-const searchKey = ref('')
-const searchResults = ref([])
-
-function onSearch() {
-  if (!searchKey.value.trim()) {
-    searchResults.value = []
-    return
-  }
-  const key = searchKey.value.trim().toLowerCase()
-  searchResults.value = warningList.value.filter(v =>
-    v.name.toLowerCase().includes(key) || v.location.toLowerCase().includes(key)
-  )
+function formatTime(value) {
+  if (!value) return '-'
+  return String(value).replace('T', ' ').slice(5, 16)
 }
 
-function locatePoint(item) {
-  if (mapInstance) {
-    mapInstance.centerAndZoom(new BMapGL.Point(item.lng, item.lat), 17)
-    openDetail(item)
-  }
-  searchResults.value = []
-  searchKey.value = ''
-}
-
-// ==================== 地图相关 ====================
+// ==================== 地图 ====================
 let mapInstance = null
 const markerMap = new Map()
 
@@ -251,22 +249,13 @@ function initMap() {
     console.warn('BMapGL 未加载或容器不存在')
     return
   }
-
   try {
     mapInstance = new BMapGL.Map('dqyjGISMap')
-    const centerPoint = new BMapGL.Point(110.393, 28.452)
-    mapInstance.centerAndZoom(centerPoint, 15)
+    mapInstance.centerAndZoom(new BMapGL.Point(110.393, 28.452), 15)
     mapInstance.enableScrollWheelZoom(true)
 
-    warningList.value.forEach(point => {
-      addMapMarker(point)
-    })
-
-    // 默认定位到第一个预警点位
-    if (warningList.value.length > 0) {
-      const firstPoint = warningList.value[0]
-      mapInstance.centerAndZoom(new BMapGL.Point(firstPoint.lng, firstPoint.lat), 15)
-    }
+    mapPoints.value.forEach(addMapMarker)
+    fitView()
   } catch (e) {
     console.error('百度地图初始化失败:', e)
   }
@@ -275,8 +264,7 @@ function initMap() {
 function addMapMarker(point) {
   const color = getLevelColor(point.level)
   const bPoint = new BMapGL.Point(point.lng, point.lat)
-
-  const html = `<div style="text-align:center;cursor:pointer;">
+  const html = `<div class="gis-marker" style="text-align:center;cursor:pointer;">
     <div style="color:#fff;background:${color};border-radius:4px;padding:2px 8px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;box-shadow:0 1px 4px rgba(0,0,0,0.3);">
       ${point.name}
     </div>
@@ -287,49 +275,50 @@ function addMapMarker(point) {
     position: bPoint,
     offset: new BMapGL.Size(-30, -50)
   })
-  label.setStyle({
-    border: 'none',
-    background: 'transparent',
-    padding: '0',
-    zIndex: '10'
-  })
+  label.setStyle({ border: 'none', background: 'transparent', padding: '0', zIndex: '10' })
   mapInstance.addOverlay(label)
   markerMap.set(point.id, label)
 
-  label.addEventListener('click', function () {
-    openDetail(point)
-  })
+  label.addEventListener('click', () => locate(point))
 }
 
-// ==================== 等级筛选 ====================
-function onLevelChange() {
-  warningList.value.forEach(point => {
-    const label = markerMap.get(point.id)
-    if (!label) return
-    const key = getLevelKey(point.level)
-    const visible = levelChecked[key]
-    label.show()
-    if (!visible) {
-      label.hide()
-    }
-  })
+/** 回到能看全所有点位的视野 */
+function fitView() {
+  if (!mapInstance || !mapPoints.value.length) return
+  const points = mapPoints.value.map((p) => new BMapGL.Point(p.lng, p.lat))
+  if (points.length === 1) {
+    mapInstance.centerAndZoom(points[0], 16)
+    return
+  }
+  mapInstance.setViewport(points)
 }
 
-// ==================== 详情弹窗 ====================
+/** 定位到点位并打开详情 */
+function locate(item) {
+  if (!item) return
+  if (mapInstance && item.lng && item.lat) {
+    mapInstance.centerAndZoom(new BMapGL.Point(item.lng, item.lat), 17)
+  }
+  if (!item.lng || !item.lat) {
+    ElMessage.warning('该预警缺少经纬度坐标,无法在地图上定位')
+  }
+  openDetail(item)
+}
+
+// ==================== 详情 ====================
 const detailVisible = ref(false)
-const currentItem = ref({})
+const currentRecord = ref(null)
 
-function openDetail(point) {
-  currentItem.value = { ...point }
+function openDetail(item) {
+  // 详情展示用后端原始记录,字段最全
+  currentRecord.value = item.raw || null
   detailVisible.value = true
 }
 
 // ==================== 生命周期 ====================
 onMounted(async () => {
   await loadData()
-  nextTick(() => {
-    initMap()
-  })
+  nextTick(() => initMap())
 })
 </script>
 
@@ -351,28 +340,23 @@ onMounted(async () => {
   position: absolute;
   top: 16px;
   left: 16px;
-  width: 280px;
-  background: rgba(6, 30, 65, 0.85);
-  border-radius: 8px;
+  bottom: 16px;
+  width: 300px;
+  display: flex;
+  flex-direction: column;
+  background: rgba(6, 30, 65, 0.88);
+  border: 1px solid rgba(90, 160, 255, 0.18);
+  border-radius: 10px;
   z-index: 100;
-  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
-  padding: 0;
-  max-height: calc(100% - 32px);
-  overflow-y: auto;
-}
-
-.side-panel::-webkit-scrollbar {
-  width: 4px;
-}
-
-.side-panel::-webkit-scrollbar-thumb {
-  background: rgba(255, 255, 255, 0.2);
-  border-radius: 2px;
+  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
+  backdrop-filter: blur(6px);
+  overflow: hidden;
 }
 
 .panel-header {
   padding: 16px 18px 12px;
   border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+  flex-shrink: 0;
 }
 
 .panel-title {
@@ -382,88 +366,104 @@ onMounted(async () => {
   letter-spacing: 1px;
 }
 
+.panel-sub {
+  margin-top: 6px;
+  font-size: 12px;
+  color: rgba(255, 255, 255, 0.55);
+}
+
+.panel-sub b {
+  color: #61c0ff;
+  font-weight: 600;
+}
+
 .panel-section {
-  padding: 14px 18px;
+  padding: 12px 18px;
   border-bottom: 1px solid rgba(255, 255, 255, 0.06);
+  flex-shrink: 0;
 }
 
-.panel-section:last-child {
+/* 清单区撑满剩余高度并内部滚动 */
+.panel-section.list-section {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
   border-bottom: none;
+  padding-bottom: 14px;
 }
 
 .section-label {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
   font-size: 12px;
-  color: rgba(255, 255, 255, 0.5);
+  color: rgba(255, 255, 255, 0.55);
   margin-bottom: 10px;
-  text-transform: uppercase;
   letter-spacing: 1px;
 }
 
-/* 等级列表 */
-.layer-list {
-  display: flex;
-  flex-direction: column;
-  gap: 6px;
-}
-
-.layer-item :deep(.el-checkbox) {
-  color: rgba(255, 255, 255, 0.85);
-  height: auto;
-}
-
-.layer-item :deep(.el-checkbox__label) {
-  color: rgba(255, 255, 255, 0.85);
-  font-size: 13px;
-  display: inline-flex;
-  align-items: center;
-  gap: 4px;
+.section-hint {
+  font-size: 11px;
+  color: rgba(255, 255, 255, 0.32);
+  letter-spacing: 0;
 }
 
-.layer-item :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
-  background: #409eff;
-  border-color: #409eff;
+.link-btn {
+  font-size: 12px;
+  color: #61c0ff;
+  cursor: pointer;
+  letter-spacing: 0;
 }
 
-.level-dot {
-  display: inline-block;
-  width: 10px;
-  height: 10px;
-  border-radius: 50%;
-  margin-right: 2px;
-  vertical-align: middle;
+.link-btn:hover {
+  text-decoration: underline;
 }
 
 /* 统计卡片 */
-.stats-row {
-  display: flex;
-  gap: 8px;
+.stat-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 6px;
 }
 
 .stat-card {
-  flex: 1;
-  background: rgba(255, 255, 255, 0.08);
+  background: rgba(255, 255, 255, 0.07);
+  border: 1px solid transparent;
   border-radius: 6px;
-  padding: 10px 4px;
+  padding: 9px 2px;
   text-align: center;
+  cursor: pointer;
+  transition: all 0.18s;
+}
+
+.stat-card:hover {
+  background: rgba(255, 255, 255, 0.13);
+}
+
+.stat-card.active {
+  border-color: rgba(97, 192, 255, 0.85);
+  background: rgba(97, 192, 255, 0.16);
 }
 
 .stat-card.stat-severe {
-  background: rgba(245, 108, 108, 0.15);
+  background: rgba(245, 108, 108, 0.14);
 }
 
 .stat-card.stat-important {
-  background: rgba(230, 162, 60, 0.15);
+  background: rgba(230, 162, 60, 0.14);
 }
 
 .stat-card.stat-normal {
-  background: rgba(64, 158, 255, 0.15);
+  background: rgba(64, 158, 255, 0.14);
 }
 
 .stat-value {
-  font-size: 20px;
+  font-size: 19px;
   font-weight: 700;
   color: #fff;
   line-height: 1.2;
+  font-variant-numeric: tabular-nums;
 }
 
 .stat-severe .stat-value {
@@ -481,17 +481,19 @@ onMounted(async () => {
 .stat-text {
   font-size: 11px;
   color: rgba(255, 255, 255, 0.5);
-  margin-top: 4px;
+  margin-top: 3px;
 }
 
-/* 搜索 */
-.side-panel :deep(.el-input__wrapper) {
+/* 深色面板下的输入控件 */
+.side-panel :deep(.el-input__wrapper),
+.side-panel :deep(.el-select__wrapper) {
   background: rgba(255, 255, 255, 0.08);
   box-shadow: none;
   border: 1px solid rgba(255, 255, 255, 0.12);
 }
 
-.side-panel :deep(.el-input__inner) {
+.side-panel :deep(.el-input__inner),
+.side-panel :deep(.el-select__placeholder) {
   color: #fff;
   font-size: 13px;
 }
@@ -500,144 +502,130 @@ onMounted(async () => {
   color: rgba(255, 255, 255, 0.35);
 }
 
-.side-panel :deep(.el-input__prefix .el-icon) {
-  color: rgba(255, 255, 255, 0.4);
+.side-panel :deep(.el-input__prefix .el-icon),
+.side-panel :deep(.el-select__caret) {
+  color: rgba(255, 255, 255, 0.45);
 }
 
-.search-results {
+/* 预警清单 */
+.warning-list {
+  flex: 1;
+  min-height: 0;
   margin-top: 8px;
-  max-height: 200px;
   overflow-y: auto;
-  border-radius: 4px;
+  margin-right: -6px;
+  padding-right: 4px;
 }
 
-.search-results::-webkit-scrollbar {
-  width: 3px;
+.warning-list::-webkit-scrollbar {
+  width: 4px;
 }
 
-.search-results::-webkit-scrollbar-thumb {
-  background: rgba(255, 255, 255, 0.15);
+.warning-list::-webkit-scrollbar-thumb {
+  background: rgba(255, 255, 255, 0.18);
   border-radius: 2px;
 }
 
-.search-item {
+.warning-item {
   display: flex;
   align-items: center;
-  padding: 7px 10px;
+  gap: 8px;
+  padding: 8px 8px;
+  border-radius: 6px;
   cursor: pointer;
-  border-radius: 4px;
   transition: background 0.15s;
 }
 
-.search-item:hover {
+.warning-item:hover {
   background: rgba(255, 255, 255, 0.1);
 }
 
-.search-name {
+/* 无坐标的记录仍可查看详情,视觉上弱化 */
+.warning-item.no-coord .wi-name,
+.warning-item.no-coord .wi-sub {
+  color: rgba(255, 255, 255, 0.42);
+}
+
+.warning-item.no-coord .level-dot {
+  opacity: 0.4;
+}
+
+.wi-main {
   flex: 1;
-  font-size: 12px;
-  color: rgba(255, 255, 255, 0.85);
-  margin-left: 6px;
+  min-width: 0;
+}
+
+.wi-name {
+  font-size: 13px;
+  color: rgba(255, 255, 255, 0.9);
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
 
-.search-level {
+.wi-sub {
+  font-size: 11px;
+  color: rgba(255, 255, 255, 0.42);
+  margin-top: 2px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.wi-level {
+  flex-shrink: 0;
   font-size: 11px;
   padding: 1px 6px;
   border-radius: 3px;
 }
 
-.search-level.level-severe {
+.list-empty {
+  text-align: center;
+  font-size: 12px;
+  color: rgba(255, 255, 255, 0.32);
+  padding: 24px 0;
+}
+
+.level-dot {
+  flex-shrink: 0;
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+}
+
+.wi-level.level-severe {
   color: #f56c6c;
   background: rgba(245, 108, 108, 0.15);
 }
 
-.search-level.level-important {
+.wi-level.level-important {
   color: #e6a23c;
   background: rgba(230, 162, 60, 0.15);
 }
 
-.search-level.level-normal {
+.wi-level.level-normal {
   color: #409eff;
   background: rgba(64, 158, 255, 0.15);
 }
 
-/* ==================== 详情弹窗样式 ==================== */
-.detail-dialog :deep(.el-dialog) {
-  border-radius: 10px;
-  overflow: hidden;
-}
-
-.detail-dialog :deep(.el-dialog__header) {
-  background: #1a1a2e;
-  padding: 14px 20px;
-  margin: 0;
+/* ==================== 地图工具条 ==================== */
+.map-tools {
+  position: absolute;
+  top: 16px;
+  right: 16px;
+  z-index: 100;
 }
 
-.detail-dialog :deep(.el-dialog__title) {
+.map-tools :deep(.el-button) {
+  background: rgba(6, 30, 65, 0.85);
+  border-color: rgba(90, 160, 255, 0.25);
   color: #fff;
-  font-size: 15px;
 }
 
-.detail-dialog :deep(.el-dialog__headerbtn .el-dialog__close) {
-  color: rgba(255, 255, 255, 0.6);
-}
-
-.detail-dialog :deep(.el-dialog__body) {
-  padding: 0;
-}
-
-.detail-content {
-  padding: 20px;
-}
-
-.detail-header {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  margin-bottom: 16px;
-  padding-bottom: 12px;
-  border-bottom: 1px solid #f0f0f0;
-}
-
-.detail-name {
-  font-size: 16px;
-  font-weight: 600;
-  color: #303133;
-}
-
-.detail-info {
-  display: flex;
-  flex-direction: column;
-}
-
-.info-row {
-  display: flex;
-  align-items: center;
-  padding: 8px 0;
-  border-bottom: 1px solid #f5f5f5;
-}
-
-.info-row:last-child {
-  border-bottom: none;
-}
-
-.info-label {
-  width: 80px;
-  flex-shrink: 0;
-  font-size: 13px;
-  color: #909399;
-}
-
-.info-value {
-  font-size: 13px;
-  color: #303133;
-}
-
-.info-value.highlight {
-  color: #f56c6c;
-  font-weight: 600;
+.map-tools :deep(.el-button:hover) {
+  background: rgba(30, 70, 130, 0.95);
+  border-color: rgba(90, 160, 255, 0.5);
+  color: #fff;
 }
 </style>

+ 343 - 144
src/views/subSystem/drainage/jcyj/dqyjgl.vue

@@ -36,157 +36,185 @@
     </div>
 
     <!-- ==================== 筛选区 ==================== -->
-    <div class="filter-panel">
-      <div class="filter-tabs">
-        <div class="tab-item" :class="{ active: activeTab === tab }" v-for="tab in tabs" :key="tab"
-          @click="handleTabChange(tab)">{{ tab }}</div>
-      </div>
-      <div class="filter-form">
-        <el-input v-model="filters.equipmentName" placeholder="预警名称" clearable style="width:150px" />
-        <el-date-picker v-model="filters.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期"
-          end-placeholder="结束日期" style="width:260px" value-format="YYYY-MM-DD" />
-        <el-select v-model="filters.warningLevel" placeholder="预警级别" clearable style="width:150px">
+    <el-form ref="queryRef" :model="filters" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item label="预警名称">
+        <el-input v-model="filters.equipmentName" placeholder="请输入预警名称" clearable style="width: 200px"
+          @keyup.enter="handleSearch" />
+      </el-form-item>
+      <el-form-item label="预警级别">
+        <el-select v-model="filters.warningLevel" placeholder="请选择预警级别" clearable style="width: 180px">
           <el-option label="严重" :value="1" />
           <el-option label="重要" :value="2" />
           <el-option label="一般" :value="3" />
         </el-select>
-        <el-button type="primary" @click="handleSearch">搜索</el-button>
-        <el-button @click="handleClear">重置</el-button>
-      </div>
-    </div>
+      </el-form-item>
+      <el-form-item label="预警类型">
+        <el-select v-model="filters.warningType" placeholder="请选择预警类型" clearable filterable style="width: 180px">
+          <el-option v-for="item in warningTypeOptions" :key="item.dictValue" :label="item.dictLabel"
+            :value="item.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="预警时间" style="width: 308px">
+        <el-date-picker v-model="filters.dateRange" type="daterange" range-separator="-"
+          start-placeholder="开始日期" end-placeholder="结束日期" style="width: 240px" value-format="YYYY-MM-DD" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
+        <el-button icon="Refresh" @click="handleClear">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- ==================== 状态分类 + 操作栏 ==================== -->
+    <el-row :gutter="10" class="mb8">
+      <el-radio-group v-model="activeTab" @change="handleTabChange">
+        <el-radio-button v-for="tab in tabs" :key="tab" :value="tab">{{ tab }}</el-radio-button>
+      </el-radio-group>
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="Refresh" :loading="loading" @click="loadData">刷新</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="loadData"></right-toolbar>
+    </el-row>
 
     <!-- ==================== 数据表格 ==================== -->
-    <div class="table-panel">
-      <el-table :data="tableData" border stripe style="width:100%" v-loading="loading">
-        <el-table-column prop="warningNo" label="预警编号" width="140" />
-        <el-table-column prop="equipmentName" label="预警名称" min-width="140" show-overflow-tooltip />
+    <div>
+      <el-table :data="tableData" style="width:100%" v-loading="loading">
+        <el-table-column prop="warningNo" label="预警编号" width="180" show-overflow-tooltip />
+        <el-table-column prop="equipmentName" label="预警名称" min-width="130" show-overflow-tooltip />
+        <el-table-column label="预警类型" width="130" align="center" show-overflow-tooltip>
+          <template #default="{ row }">{{ warningTypeText(row) }}</template>
+        </el-table-column>
         <el-table-column prop="warningLevel" label="预警级别" width="90" align="center">
           <template #default="{ row }">
             <el-tag :type="levelTagType(row.warningLevel)" size="small">{{ levelText(row.warningLevel) }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="deviceCode" label="预警类型" width="110" align="center">
-          <template #default="{ row }">
-            {{ equipmentTypeMap.get(String(row.deviceCode)) || row.deviceCode || row.warningType || '-' }}
-          </template>
+        <el-table-column label="预警内容" min-width="200" show-overflow-tooltip>
+          <template #default="{ row }">{{ row.content || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="ownershipUnit" label="权属单位" width="140" show-overflow-tooltip>
+          <template #default="{ row }">{{ row.ownershipUnit || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="location" label="位置" min-width="150" show-overflow-tooltip>
+          <template #default="{ row }">{{ row.location || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="预警时间" width="170">
+          <template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
+        </el-table-column>
+        <el-table-column prop="publisher" label="发布人" width="110" show-overflow-tooltip>
+          <template #default="{ row }">{{ row.publisher || '-' }}</template>
         </el-table-column>
-        <el-table-column prop="warningSpecial" label="预警专项" width="110" align="center" show-overflow-tooltip />
-        <el-table-column prop="location" label="位置" min-width="160" show-overflow-tooltip />
-        <el-table-column prop="createTime" label="预警时间" width="160" />
         <el-table-column prop="status" label="状态" width="90" align="center">
           <template #default="{ row }">
             <el-tag :type="statusTagType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="220" fixed="right">
+        <el-table-column label="操作" width="170" fixed="right">
           <template #default="{ row }">
             <el-button type="primary" link size="small" @click="handleView(row)">查看</el-button>
-            <!-- <el-button type="info" link size="small" @click="handleClose(row)"
-              v-if="row.status !== 'CLOSED' && row.status !== 'HANDLED'">关闭</el-button> -->
+            <el-button v-if="row.status !== 'HANDLED' && row.status !== 'CLOSED'" type="warning" link size="small"
+              @click="handleClose(row)">解除</el-button>
             <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
-      <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :page-sizes="[10, 20, 50, 100]"
-        :total="total" layout="total, sizes, prev, pager, next, jumper"
-        style="margin-top: 16px; justify-content: flex-end" @size-change="loadData" @current-change="loadData" />
+      <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize"
+        @pagination="loadData" />
     </div>
 
     <!-- ==================== 查看详情弹窗 ==================== -->
-    <el-drawer v-model="detailVisible" title="预警详情" size="650px" :close-on-click-modal="false" class="detail-drawer" direction="rtl">
-      <div class="detail-content" v-if="currentOrder">
-        <!-- 预警信息 -->
+    <el-drawer v-model="detailVisible" title="预警详情" size="720px" :close-on-click-modal="false"
+      class="detail-drawer" direction="rtl">
+      <div class="detail-content" v-if="currentOrder" v-loading="detailLoading">
         <div class="section-box">
           <div class="section-title">预警信息</div>
-          <el-form label-width="90px" size="default">
-            <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="预警编号">{{ currentOrder.warningNo }}</el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="预警名称">{{ currentOrder.equipmentName }}</el-form-item>
-              </el-col>
-            </el-row>
-            <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="预警级别">
-                  <el-tag :type="levelTagType(currentOrder.warningLevel)" size="small">{{
-                    levelText(currentOrder.warningLevel) }}</el-tag>
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="预警类型">{{ equipmentTypeMap.get(String(currentOrder.deviceCode)) ||
-                  currentOrder.deviceCode || currentOrder.warningType }}</el-form-item>
-              </el-col>
-            </el-row>
-            <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="预警专项">{{ currentOrder.warningSpecial || '-' }}</el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="状态">
-                  <el-tag :type="statusTagType(currentOrder.status)" size="small">{{ statusText(currentOrder.status)
-                  }}</el-tag>
-                </el-form-item>
-              </el-col>
-            </el-row>
-            <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="预警时间">{{ currentOrder.createTime }}</el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="发布人">{{ currentOrder.publisher || '-' }}</el-form-item>
-              </el-col>
-            </el-row>
-            <el-form-item label="位置">
-              <div class="desc-text">{{ currentOrder.location }}</div>
-            </el-form-item>
-          </el-form>
+          <el-descriptions :column="2" border size="small">
+            <el-descriptions-item label="预警编号">{{ currentOrder.warningNo || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="预警名称">{{ currentOrder.equipmentName || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="预警类型">{{ warningTypeText(currentOrder) }}</el-descriptions-item>
+            <el-descriptions-item label="预警级别">
+              <el-tag :type="levelTagType(currentOrder.warningLevel)" size="small">{{ levelText(currentOrder.warningLevel)
+                }}</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="预警专项">{{ currentOrder.warningSpecial || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="权属单位">{{ currentOrder.ownershipUnit || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="预警时间">{{ formatDateTime(currentOrder.createTime) }}</el-descriptions-item>
+            <el-descriptions-item label="状态">
+              <el-tag :type="statusTagType(currentOrder.status)" size="small">{{ statusText(currentOrder.status)
+                }}</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="发布人">{{ currentOrder.publisher || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="创建人">{{ currentOrder.createBy || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="位置" :span="2">{{ currentOrder.location || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="预警内容" :span="2">
+              <div class="desc-text">{{ currentOrder.content || '-' }}</div>
+            </el-descriptions-item>
+            <el-descriptions-item v-if="currentOrder.remark" label="备注" :span="2">{{ currentOrder.remark }}
+            </el-descriptions-item>
+          </el-descriptions>
         </div>
 
-        <!-- 人员安排 -->
-        <!-- <div class="section-box">
-          <div class="section-title">
-            接收人员安排
-            <el-button type="primary" size="small" style="float:right" @click="showAddPersonDialog">添加</el-button>
+        <div class="section-box">
+          <div class="section-title">关联设备与预警指标</div>
+          <el-descriptions v-if="detailDevice" :column="2" border size="small">
+            <el-descriptions-item label="设备名称">{{ detailDevice.equipmentName || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="设备编码">{{ detailDevice.equipmentCode || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="设备类型">{{ equipmentTypeMap.get(String(detailDevice.equipmentTypeId)) || '-'
+              }}</el-descriptions-item>
+            <el-descriptions-item label="在线状态">
+              <el-tag :type="detailDevice.onlineStatus === 1 ? 'success' : 'info'" size="small">
+                {{ detailDevice.onlineStatus === 1 ? '在线' : '离线' }}
+              </el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="安装位置" :span="2">{{ detailDevice.equipmentLocation ||
+              currentOrder.location || '-' }}</el-descriptions-item>
+          </el-descriptions>
+          <el-alert v-else type="info" :closable="false" show-icon class="device-missing"
+            title="未在设备台账中找到该预警关联的设备,实时指标无法展示" />
+
+          <div class="metric-grid">
+            <div class="metric-card">
+              <div class="metric-label">实时液位</div>
+              <div class="metric-value">
+                <template v-if="detailMetric.level !== null">{{ detailMetric.level
+                  }}<span class="metric-unit">m</span></template>
+                <span v-else class="metric-empty">—</span>
+              </div>
+            </div>
+            <div class="metric-card">
+              <div class="metric-label">报警阈值</div>
+              <div class="metric-value metric-value-text">{{ detailMetric.thresholdText || '未配置' }}</div>
+            </div>
+            <div class="metric-card">
+              <div class="metric-label">采集时间</div>
+              <div class="metric-value metric-value-text">{{ detailMetric.collectTime || '—' }}</div>
+            </div>
           </div>
-          <el-table :data="currentOrder.personnel" border size="small" style="width:100%">
-            <el-table-column prop="name" label="姓名" />
-            <el-table-column prop="phone" label="联系方式" />
-            <el-table-column prop="dept" label="部门" />
-            <el-table-column label="操作" width="80" align="center">
-              <template #default="{ $index }">
-                <el-button link type="danger" size="small"
-                  @click="currentOrder.personnel.splice($index, 1)">删除</el-button>
-              </template>
+
+          <el-table v-if="detailThresholds.length" :data="detailThresholds" border size="small"
+            empty-text="该设备尚未配置报警阈值">
+            <el-table-column label="监测指标" prop="warningName" min-width="120" />
+            <el-table-column label="单位" width="90" align="center">
+              <template #default="{ row }">{{ row.warningUnit || '-' }}</template>
+            </el-table-column>
+            <el-table-column label="阈值范围" min-width="150" align="center">
+              <template #default="{ row }">{{ formatThreshold(row) }}</template>
             </el-table-column>
           </el-table>
-        </div> -->
+        </div>
+
+        <div class="section-box">
+          <div class="section-title">处置人员</div>
+          <el-descriptions :column="2" border size="small">
+            <el-descriptions-item label="处置人">{{ currentOrder.processor || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="发布人">{{ currentOrder.publisher || '-' }}</el-descriptions-item>
+          </el-descriptions>
+        </div>
       </div>
 
       <template #footer>
         <el-button @click="detailVisible = false">关 闭</el-button>
       </template>
     </el-drawer>
-
-    <!-- 添加人员弹窗 -->
-    <el-dialog v-model="addPersonVisible" title="添加人员" width="450px">
-      <el-form :model="personForm" :rules="personRules" ref="personFormRef" label-width="80px">
-        <el-form-item label="姓名" prop="name">
-          <el-input v-model="personForm.name" placeholder="请输入姓名" />
-        </el-form-item>
-        <el-form-item label="联系方式" prop="phone">
-          <el-input v-model="personForm.phone" placeholder="请输入联系方式" />
-        </el-form-item>
-        <el-form-item label="部门" prop="dept">
-          <el-input v-model="personForm.dept" placeholder="请输入部门" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="addPersonVisible = false">取 消</el-button>
-        <el-button type="primary" @click="handleAddPersonConfirm">确 定</el-button>
-      </template>
-    </el-dialog>
   </div>
 </template>
 
@@ -195,7 +223,10 @@ import { ref, computed, onMounted, nextTick } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { Promotion } from '@element-plus/icons-vue'
 import * as echarts from 'echarts'
-import { getWarningCurrentPage, updateWarningStatus, deleteWarning, getEquipmentTypeList } from '@/api/drainage'
+import { getWarningCurrentPage, updateWarningStatus, deleteWarning, getEquipmentTypeList, getLatestWaterLevelByDevice } from '@/api/drainage'
+import { listEquipment } from '@/api/pipeNetwork/pdeviceInform'
+import { pageWarningThresholds } from '@/api/warningThreshold'
+import { listWarningTypes } from '@/api/warning/list'
 import useUserStore from '@/store/modules/user'
 
 // ==================== 用户信息 ====================
@@ -227,8 +258,12 @@ function statusTagType(status) {
   return map[status] || 'info'
 }
 
-// ==================== 设备类型映射 ====================
+// ==================== 设备类型 / 预警类型映射 ====================
 const equipmentTypeMap = ref(new Map())
+/** warningType(类型编码) -> 类型名称,来自 warning_type 表(与生命线预警系统同一份配置) */
+const warningTypeMap = ref(new Map())
+/** 预警类型筛选项原样保留,下拉直接用 */
+const warningTypeOptions = ref([])
 
 async function loadEquipmentTypes() {
   try {
@@ -246,6 +281,33 @@ async function loadEquipmentTypes() {
   }
 }
 
+async function loadWarningTypes() {
+  try {
+    const res = await listWarningTypes()
+    const list = res?.data || res?.rows || []
+    const map = new Map()
+    if (Array.isArray(list)) {
+      list.forEach(item => {
+        if (item.dictValue) map.set(String(item.dictValue), item.dictLabel || item.dictValue)
+      })
+    }
+    warningTypeOptions.value = Array.isArray(list) ? list : []
+    warningTypeMap.value = map
+  } catch (error) {
+    console.warn('加载预警类型配置失败', error)
+  }
+}
+
+/** 预警类型展示:新数据看 warningType,历史数据回退到 deviceCode 的设备类型名 */
+function warningTypeText(row) {
+  if (!row) return '-'
+  const byType = row.warningType ? warningTypeMap.value.get(String(row.warningType)) : null
+  if (byType) return byType
+  const byDeviceType = row.deviceCode ? equipmentTypeMap.value.get(String(row.deviceCode)) : null
+  if (byDeviceType) return byDeviceType
+  return row.warningType || row.deviceCode || '-'
+}
+
 // ==================== 统计指标 ====================
 const statItems = computed(() => {
   const list = tableData.value
@@ -270,11 +332,11 @@ const levelLegends = computed(() => [
   { name: '一般', value: tableData.value.filter(o => Number(o.warningLevel) === 3).length, color: '#409EFF' }
 ])
 const typeLegends = computed(() => {
-  const types = [...new Set(tableData.value.map(o => o.deviceCode || o.warningType).filter(Boolean))]
+  const types = [...new Set(tableData.value.map(o => warningTypeText(o)).filter(t => t && t !== '-'))]
   const colors = ['#409EFF', '#E6A23C', '#67C23A', '#F56C6C']
   return types.map((t, i) => ({
-    name: equipmentTypeMap.value.get(String(t)) || t,
-    value: tableData.value.filter(o => (o.deviceCode || o.warningType) === t).length,
+    name: t,
+    value: tableData.value.filter(o => warningTypeText(o) === t).length,
     color: colors[i % colors.length]
   }))
 })
@@ -287,10 +349,13 @@ const ringCharts = [
 // ==================== 筛选 ====================
 const tabs = ['全部', '草稿', '待办', '处置中', '已发布']
 const activeTab = ref('全部')
+const showSearch = ref(true)
+const queryRef = ref()
 const filters = ref({
   equipmentName: '',
   dateRange: null,
-  warningLevel: ''
+  warningLevel: '',
+  warningType: ''
 })
 
 // ==================== 数据加载 ====================
@@ -302,6 +367,7 @@ async function loadData() {
     }
     if (filters.value.equipmentName) params.equipmentName = filters.value.equipmentName
     if (filters.value.warningLevel) params.warningLevel = filters.value.warningLevel
+    if (filters.value.warningType) params.warningType = filters.value.warningType
     if (filters.value.dateRange && filters.value.dateRange.length === 2) {
       params.startTime = `${filters.value.dateRange[0]} 00:00:00`
       params.endTime = `${filters.value.dateRange[1]} 23:59:59`
@@ -339,7 +405,7 @@ function handleSearch() {
 }
 
 function handleClear() {
-  filters.value = { equipmentName: '', dateRange: null, warningLevel: '' }
+  filters.value = { equipmentName: '', dateRange: null, warningLevel: '', warningType: '' }
   activeTab.value = '全部'
   pageNum.value = 1
   loadData()
@@ -354,9 +420,13 @@ function handleTabChange(tab) {
 // ==================== 操作:关闭/删除 ====================
 async function handleClose(row) {
   try {
-    await ElMessageBox.confirm('确认关闭该预警?', '提示', { type: 'warning' })
-    await updateWarningStatus(row.id, { status: 'CLOSED' })
-    ElMessage.success('预警已关闭')
+    await ElMessageBox.confirm('确认解除该预警?解除后会进入历史预警。', '提示', { type: 'warning' })
+    // 处置人一并写入,否则历史预警的「处置人」列永远是空
+    await updateWarningStatus(row.id, {
+      status: 'CLOSED',
+      processor: userStore.nickName || userStore.name || ''
+    })
+    ElMessage.success('预警已解除')
     loadData()
   } catch (error) {
     if (error !== 'cancel') console.error('关闭预警失败', error)
@@ -377,37 +447,117 @@ async function handleDelete(row) {
 // ==================== 详情弹窗 ====================
 const detailVisible = ref(false)
 const currentOrder = ref(null)
+const detailLoading = ref(false)
+const detailDevice = ref(null)
+const detailThresholds = ref([])
+const waterLevelMap = ref({})
+const thresholdMap = ref({})
+/** equipmentName -> 设备档案,排水预警 device_code 存的是类型 id,只能按名称反查设备 */
+const deviceByName = ref(null)
+
+/** 实时液位/阈值都是从设备档案派生的,统一在这里算,模板里直接取值 */
+const detailMetric = computed(() => {
+  const device = detailDevice.value
+  const code = device?.equipmentCode
+  const levelItem = code ? waterLevelMap.value[code] : null
+  const thresholds = code ? (thresholdMap.value[code] || []) : []
+  return {
+    level: levelItem && levelItem.value !== undefined && levelItem.value !== null ? levelItem.value : null,
+    collectTime: levelItem?.time ? formatDateTime(levelItem.time) : '',
+    thresholdText: thresholds.length > 0 ? thresholds.map(formatThreshold).join(' / ') : ''
+  }
+})
 
-function handleView(row) {
-  currentOrder.value = { ...row, personnel: [] }
+async function handleView(row) {
+  currentOrder.value = { ...row }
+  detailDevice.value = null
+  detailThresholds.value = []
   detailVisible.value = true
+  detailLoading.value = true
+  try {
+    const [device] = await Promise.all([resolveDevice(row.equipmentName), loadLiveMetrics()])
+    detailDevice.value = device || null
+    const code = device?.equipmentCode
+    detailThresholds.value = code ? (thresholdMap.value[code] || []) : []
+  } catch (error) {
+    console.warn('预警详情数据加载失败', error)
+  } finally {
+    detailLoading.value = false
+  }
 }
 
-// ==================== 添加人员弹窗 ====================
-const addPersonVisible = ref(false)
-const personFormRef = ref(null)
-const personForm = ref({ name: '', phone: '', dept: '' })
-const personRules = {
-  name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
-  phone: [{ required: true, message: '请输入联系方式', trigger: 'blur' }],
-  dept: [{ required: true, message: '请输入部门', trigger: 'blur' }]
+/** 按设备名称在排水设备台账里反查设备(一次加载后缓存) */
+async function resolveDevice(equipmentName) {
+  if (!equipmentName) return null
+  if (!deviceByName.value) {
+    const map = new Map()
+    let pageNum = 1
+    while (pageNum <= 10) {
+      const res = await listEquipment({ rootTypeId: 2, pageNum, pageSize: 100 })
+      const page = res?.data && Array.isArray(res.data.records) ? res.data : res
+      const rows = page?.records || page?.rows || []
+      if (rows.length === 0) break
+      rows.forEach((item) => {
+        if (item?.equipmentName && !map.has(item.equipmentName)) map.set(item.equipmentName, item)
+      })
+      if (rows.length < 100) break
+      pageNum += 1
+    }
+    deviceByName.value = map
+  }
+  return deviceByName.value.get(equipmentName) || null
+}
+
+/** 实时液位 + 报警阈值,都按设备编码索引 */
+async function loadLiveMetrics() {
+  const [levelResult, thresholdResult] = await Promise.allSettled([
+    getLatestWaterLevelByDevice(),
+    pageWarningThresholds({ rootTypeId: 2, pageNum: 1, pageSize: 500 })
+  ])
+
+  if (levelResult.status === 'fulfilled') {
+    const list = Array.isArray(levelResult.value) ? levelResult.value : (levelResult.value?.data || [])
+    const map = {}
+    list.forEach((item) => {
+      if (!item?.deviceCode) return
+      map[item.deviceCode] = { value: item.waterLevel, time: item.createTime }
+    })
+    waterLevelMap.value = map
+  }
+
+  if (thresholdResult.status === 'fulfilled') {
+    const res = thresholdResult.value
+    const page = res?.data && Array.isArray(res.data.records) ? res.data : res
+    const rows = page?.records || page?.rows || []
+    const map = {}
+    rows.forEach((row) => {
+      if (!row?.deviceCode) return
+      if (!map[row.deviceCode]) map[row.deviceCode] = []
+      map[row.deviceCode].push(row)
+    })
+    thresholdMap.value = map
+  }
 }
 
-function showAddPersonDialog() {
-  personForm.value = { name: '', phone: '', dept: '' }
-  addPersonVisible.value = true
+/** 阈值展示:dataType 0 区间 / 1 下限 / 2 上限(字段与报警阈值配置接口一致) */
+function formatThreshold(item) {
+  if (!item) return '-'
+  const unit = item.warningUnit || ''
+  const min = item.minValue
+  const max = item.maxValue
+  if (item.dataType === 1) return min === null || min === undefined ? '-' : `≥ ${min}${unit}`
+  if (item.dataType === 2) return max === null || max === undefined ? '-' : `≤ ${max}${unit}`
+  if ((min === null || min === undefined) && (max === null || max === undefined)) return '未设置'
+  return `${min ?? '-'} ~ ${max ?? '-'}${unit}`
 }
 
-function handleAddPersonConfirm() {
-  personFormRef.value.validate(valid => {
-    if (!valid) return
-    if (currentOrder.value) {
-      currentOrder.value.personnel.push({ ...personForm.value })
-    }
-    addPersonVisible.value = false
-  })
+function formatDateTime(value) {
+  if (!value) return '-'
+  return String(value).replace('T', ' ').slice(0, 19)
 }
 
+
+
 // ==================== ECharts 初始化 ====================
 function initCharts() {
   ringCharts.forEach(chart => {
@@ -440,7 +590,7 @@ function initCharts() {
 }
 
 onMounted(async () => {
-  await loadEquipmentTypes()
+  await Promise.all([loadEquipmentTypes(), loadWarningTypes()])
   loadData()
 })
 </script>
@@ -676,6 +826,55 @@ onMounted(async () => {
   overflow-y: auto;
 }
 
+/* 预警指标实时数据卡片 */
+.metric-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 12px;
+  margin: 12px 0;
+}
+
+.metric-card {
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 12px 14px;
+  background: #fafbfc;
+}
+
+.metric-label {
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 6px;
+}
+
+.metric-value {
+  font-size: 22px;
+  font-weight: 600;
+  color: #303133;
+  line-height: 1.2;
+}
+
+.metric-value-text {
+  font-size: 14px;
+  font-weight: 500;
+  word-break: break-all;
+}
+
+.metric-unit {
+  font-size: 12px;
+  font-weight: 400;
+  color: #909399;
+  margin-left: 2px;
+}
+
+.metric-empty {
+  color: #c0c4cc;
+}
+
+.device-missing {
+  margin-bottom: 12px;
+}
+
 :deep(.el-form-item__label) {
   color: #909399;
 }

+ 386 - 359
src/views/subSystem/drainage/jcyj/lsyjGIS.vue

@@ -1,188 +1,236 @@
 <template>
   <div class="gis-page">
-    <!-- 左侧浮动面板 -->
-    <div class="side-panel">
+    <!-- ==================== 左侧浮动面板 ==================== -->
+    <aside class="side-panel">
       <div class="panel-header">
-        <span class="panel-title">历史预警分布</span>
-      </div>
-
-      <!-- 等级筛选 -->
-      <div class="panel-section">
-        <div class="section-label">等级筛选</div>
-        <div class="layer-list">
-          <div class="layer-item">
-            <el-checkbox v-model="levelChecked.severe" @change="onLevelChange">
-              <span class="level-dot" style="background:#f56c6c"></span> 严重
-            </el-checkbox>
-          </div>
-          <div class="layer-item">
-            <el-checkbox v-model="levelChecked.important" @change="onLevelChange">
-              <span class="level-dot" style="background:#e6a23c"></span> 重要
-            </el-checkbox>
-          </div>
-          <div class="layer-item">
-            <el-checkbox v-model="levelChecked.normal" @change="onLevelChange">
-              <span class="level-dot" style="background:#409eff"></span> 一般
-            </el-checkbox>
-          </div>
+        <div class="panel-title">历史预警分布</div>
+        <div class="panel-sub">
+          共 <b>{{ warningList.length }}</b> 条 · 已上图 <b>{{ mapPoints.length }}</b> 条
         </div>
       </div>
 
-      <!-- 统计数据 -->
+      <!-- 概览(点击卡片按等级筛选) -->
       <div class="panel-section">
-        <div class="section-label">预警统计</div>
-        <div class="stats-row">
-          <div class="stat-card">
-            <div class="stat-value">{{ warningList.length }}</div>
-            <div class="stat-text">总数</div>
+        <div class="section-label">
+          预警概览
+          <span class="section-hint">点击卡片筛选等级</span>
+        </div>
+        <div class="stat-grid">
+          <div class="stat-card" :class="{ active: levelFilter === 'all' }" @click="setLevel('all')">
+            <div class="stat-value">{{ rawList.length }}</div>
+            <div class="stat-text">全部</div>
           </div>
-          <div class="stat-card stat-severe">
+          <div class="stat-card stat-severe" :class="{ active: levelFilter === 'severe' }" @click="setLevel('severe')">
             <div class="stat-value">{{ severeCount }}</div>
             <div class="stat-text">严重</div>
           </div>
-          <div class="stat-card stat-important">
+          <div class="stat-card stat-important" :class="{ active: levelFilter === 'important' }"
+            @click="setLevel('important')">
             <div class="stat-value">{{ importantCount }}</div>
             <div class="stat-text">重要</div>
           </div>
-          <div class="stat-card stat-normal">
+          <div class="stat-card stat-normal" :class="{ active: levelFilter === 'normal' }" @click="setLevel('normal')">
             <div class="stat-value">{{ normalCount }}</div>
             <div class="stat-text">一般</div>
           </div>
         </div>
       </div>
 
-      <!-- 搜索框 -->
+      <!-- 筛选 -->
       <div class="panel-section">
-        <div class="section-label">快速定位</div>
-        <el-input
-          v-model="searchKey"
-          placeholder="输入设备名称搜索"
-          clearable
-          prefix-icon="Search"
-          @input="onSearch"
-        />
-        <div class="search-results" v-if="searchResults.length > 0">
-          <div
-            class="search-item"
-            v-for="item in searchResults"
-            :key="item.id"
-            @click="locatePoint(item)"
-          >
-            <span class="level-dot" :style="{ background: getLevelColor(item.level) }"></span>
-            <span class="search-name">{{ item.name }}</span>
-            <span class="search-level" :class="getLevelClass(item.level)">{{ item.level }}</span>
-          </div>
+        <div class="section-label">
+          筛选条件
+          <span class="link-btn" v-if="typeFilter || statusFilter || levelFilter !== 'all'" @click="resetFilter">重置</span>
+        </div>
+        <div class="filter-row">
+          <el-select v-model="typeFilter" placeholder="全部类型" clearable filterable size="small" style="flex:1">
+            <el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+          <el-select v-model="statusFilter" placeholder="全部状态" clearable size="small"
+            style="width:106px;flex-shrink:0">
+            <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
         </div>
       </div>
-    </div>
 
-    <!-- 地图容器 -->
-    <div id="lsyjGISMap" class="map-container"></div>
-
-    <!-- 详情弹窗 -->
-    <el-dialog
-      v-model="detailVisible"
-      :title="currentItem.name"
-      width="480px"
-      :close-on-click-modal="false"
-      class="detail-dialog"
-    >
-      <div class="detail-content">
-        <div class="detail-header">
-          <el-tag :type="getTagType(currentItem.level)" effect="dark" size="small">
-            {{ currentItem.level }}
-          </el-tag>
-          <span class="detail-name">{{ currentItem.name }}</span>
+      <!-- 预警清单 -->
+      <div class="panel-section list-section">
+        <div class="section-label">
+          归档清单
+          <span class="section-hint">{{ displayList.length }} 条</span>
         </div>
-        <div class="detail-info">
-          <div class="info-row">
-            <span class="info-label">预警编号</span>
-            <span class="info-value">{{ currentItem.warningNo }}</span>
-          </div>
-          <div class="info-row">
-            <span class="info-label">预警级别</span>
-            <span class="info-value">
-              <el-tag :type="getTagType(currentItem.level)" effect="dark" size="small">{{ currentItem.level }}</el-tag>
-            </span>
-          </div>
-          <div class="info-row">
-            <span class="info-label">预警时间</span>
-            <span class="info-value">{{ currentItem.createTime }}</span>
-          </div>
-          <div class="info-row">
-            <span class="info-label">位置</span>
-            <span class="info-value">{{ currentItem.location }}</span>
-          </div>
-          <div class="info-row">
-            <span class="info-label">处理状态</span>
-            <span class="info-value">
-              <el-tag :type="getStatusTagType(currentItem.statusKey)" size="small">{{ currentItem.statusText }}</el-tag>
-            </span>
+        <el-input v-model="searchKey" placeholder="搜索名称 / 位置" clearable size="small" :prefix-icon="Search" />
+        <div class="warning-list">
+          <div class="warning-item" v-for="item in displayList" :key="item.id" :class="{ 'no-coord': !item.lng }"
+            @click="locate(item)">
+            <span class="level-dot" :style="{ background: getLevelColor(item.level) }"></span>
+            <div class="wi-main">
+              <div class="wi-name" :title="item.name">{{ item.name }}</div>
+              <div class="wi-sub">归档于 {{ formatTime(item.archiveTime) }}</div>
+            </div>
+            <div class="wi-right">
+              <span class="wi-level" :class="getLevelClass(item.level)">{{ item.level }}</span>
+              <span class="wi-status" :class="`st-${item.statusKey}`">{{ item.statusText }}</span>
+            </div>
           </div>
+          <div class="list-empty" v-if="!displayList.length">暂无符合条件的历史预警</div>
         </div>
       </div>
-    </el-dialog>
+    </aside>
+
+    <!-- ==================== 地图容器 ==================== -->
+    <div id="lsyjGISMap" class="map-container"></div>
+
+    <!-- 地图工具条 -->
+    <div class="map-tools">
+      <el-tooltip content="回到预警范围" placement="left">
+        <el-button circle @click="fitView"><el-icon><Aim /></el-icon></el-button>
+      </el-tooltip>
+    </div>
+
+    <!-- ==================== 详情抽屉 ==================== -->
+    <WarningGisDetail v-model="detailVisible" :record="currentRecord" mode="history" />
   </div>
 </template>
 
 <script setup name="LsyjglGIS">
-import { ref, reactive, computed, onMounted, nextTick } from 'vue'
-import { Search } from '@element-plus/icons-vue'
+import { ref, computed, watch, onMounted, nextTick } from 'vue'
+import { Search, Aim } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 import locationIcon from '@/assets/images/location.png'
 import { getWarningHistoryPage } from '@/api/drainage'
+import { listWarningTypes } from '@/api/warning/list'
+import WarningGisDetail from './components/WarningGisDetail.vue'
 
-// ==================== 等级筛选 ====================
-const levelChecked = reactive({ severe: true, important: true, normal: true })
-
-// ==================== 历史预警数据(从API加载)====================
+// ==================== 数据 ====================
+const rawList = ref([])
+/** 归一化后的全部预警(含无坐标的,清单里照样能看详情) */
 const warningList = ref([])
 
-// 预警级别/状态数字→文字映射
+/** 能上图的点位 */
+const mapPoints = computed(() => warningList.value.filter((p) => p.lng && p.lat))
+
 const levelMap = { 1: '严重', 2: '重要', 3: '一般' }
-const statusMap = { HANDLED: '已处置', CLOSED: '已解除', DRAFT: '草稿', PENDING: '待办', RECEIVED: '已接收', PROCESSING: '处置中' }
+const levelKeyMap = { 1: 'severe', 2: 'important', 3: 'normal' }
+const statusMap = {
+  DRAFT: '草稿', PENDING: '待办', RECEIVED: '已接收',
+  PROCESSING: '处置中', HANDLED: '已处置', CLOSED: '已解除'
+}
+
+const typeOptions = ref([])
+const statusOptions = ref([])
+const warningTypeMap = ref(new Map())
+
+async function loadWarningTypes() {
+  try {
+    const res = await listWarningTypes()
+    const list = res?.data || res?.rows || []
+    const map = new Map()
+    if (Array.isArray(list)) {
+      list.forEach((item) => {
+        if (item?.dictValue) map.set(String(item.dictValue), item.dictLabel || item.dictValue)
+      })
+    }
+    warningTypeMap.value = map
+  } catch (error) {
+    console.warn('加载预警类型配置失败', error)
+  }
+}
 
-// 加载历史预警数据
 async function loadData() {
   try {
-    const res = await getWarningHistoryPage(1, 1000, {
-      historyOnly: true,
-      equipmentType: 'drainage'
-    })
+    const [res] = await Promise.all([
+      getWarningHistoryPage(1, 1000, { historyOnly: true, equipmentType: 'drainage' }),
+      loadWarningTypes()
+    ])
     const pageData = res.code !== undefined ? res.data : res
     const records = pageData.records || []
-    const hasCoordCount = records.filter(w => w.longitude && w.latitude).length
-    console.log('[历史预警GIS] API返回记录数:', records.length, '有坐标记录数:', hasCoordCount)
-    if (records.length > 0 && hasCoordCount === 0) {
-      console.warn('[历史预警GIS] 所有预警均无经纬度坐标,请检查发布预警时设备是否有定位数据')
-    }
-    const noCoordRecords = records.filter(w => !w.longitude || !w.latitude)
-    if (noCoordRecords.length > 0) {
-      console.warn('[历史预警GIS] 无坐标预警(将被过滤):', noCoordRecords.map(w => ({ id: w.id, name: w.equipmentName, longitude: w.longitude, latitude: w.latitude })))
+    rawList.value = records
+    warningList.value = records.map(normalize)
+
+    // 类型下拉:按记录里实际出现的类型构建
+    const types = [...new Set(records.map((r) => r.warningType).filter(Boolean))]
+    typeOptions.value = types.map((val) => ({
+      value: String(val),
+      label: warningTypeMap.value.get(String(val)) || String(val)
+    }))
+
+    // 状态下拉
+    const statuses = [...new Set(records.map((r) => r.status).filter(Boolean))]
+    statusOptions.value = statuses.map((val) => ({ value: val, label: statusMap[val] || val }))
+
+    const noCoord = records.length - warningList.value.filter((p) => p.lng && p.lat).length
+    if (noCoord > 0) {
+      console.warn(`[历史预警GIS] ${noCoord} 条预警缺少经纬度,未上图(清单中仍可查看详情)`)
     }
-    warningList.value = records.map(w => ({
-      id: w.id,
-      name: w.equipmentName || '-',
-      warningNo: w.warningNo || '-',
-      deviceCode: w.deviceCode || w.warningType || '',
-      level: levelMap[w.warningLevel] || '一般',
-      createTime: w.createTime || '-',
-      location: w.location || '-',
-      statusKey: w.status || '',
-      statusText: statusMap[w.status] || '-',
-      lng: parseFloat(w.longitude),
-      lat: parseFloat(w.latitude)
-    })).filter(w => w.lng && w.lat)
   } catch (error) {
     console.error('加载历史预警数据失败', error)
     ElMessage.error('加载历史预警数据失败')
   }
 }
 
-// ==================== 统计计算 ====================
-const severeCount = computed(() => warningList.value.filter(v => v.level === '严重').length)
-const importantCount = computed(() => warningList.value.filter(v => v.level === '重要').length)
-const normalCount = computed(() => warningList.value.filter(v => v.level === '一般').length)
+function normalize(w) {
+  return {
+    id: w.id,
+    name: w.equipmentName || '-',
+    level: levelMap[w.warningLevel] || '一般',
+    levelKey: levelKeyMap[w.warningLevel] || 'normal',
+    typeValue: w.warningType ? String(w.warningType) : '',
+    typeText: w.warningType ? (warningTypeMap.value.get(String(w.warningType)) || String(w.warningType)) : '未分类',
+    statusValue: w.status || '',
+    statusKey: String(w.status || 'unknown').toLowerCase(),
+    statusText: statusMap[w.status] || '-',
+    createTime: w.createTime || '',
+    archiveTime: w.updateTime || w.publishTime || w.createTime || '',
+    location: w.location || '',
+    raw: w,
+    lng: parseFloat(w.longitude),
+    lat: parseFloat(w.latitude)
+  }
+}
+
+// ==================== 统计 ====================
+const severeCount = computed(() => rawList.value.filter((v) => v.warningLevel === 1).length)
+const importantCount = computed(() => rawList.value.filter((v) => v.warningLevel === 2).length)
+const normalCount = computed(() => rawList.value.filter((v) => v.warningLevel === 3).length)
+
+// ==================== 筛选 ====================
+const levelFilter = ref('all')
+const typeFilter = ref('')
+const statusFilter = ref('')
+const searchKey = ref('')
+
+const displayList = computed(() => {
+  const key = searchKey.value.trim().toLowerCase()
+  return warningList.value.filter((item) => {
+    if (levelFilter.value !== 'all' && item.levelKey !== levelFilter.value) return false
+    if (typeFilter.value && item.typeValue !== typeFilter.value) return false
+    if (statusFilter.value && item.statusValue !== statusFilter.value) return false
+    if (key && !(item.name.toLowerCase().includes(key) || item.location.toLowerCase().includes(key))) return false
+    return true
+  })
+})
+
+function setLevel(key) {
+  levelFilter.value = levelFilter.value === key ? 'all' : key
+}
+
+function resetFilter() {
+  levelFilter.value = 'all'
+  typeFilter.value = ''
+  statusFilter.value = ''
+  searchKey.value = ''
+}
+
+watch(displayList, () => {
+  const visibleIds = new Set(displayList.value.map((item) => item.id))
+  warningList.value.forEach((point) => {
+    const label = markerMap.get(point.id)
+    if (!label) return
+    if (visibleIds.has(point.id)) label.show()
+    else label.hide()
+  })
+})
 
 // ==================== 工具方法 ====================
 function getLevelColor(level) {
@@ -195,46 +243,12 @@ function getLevelClass(level) {
   return map[level] || ''
 }
 
-function getTagType(level) {
-  const map = { '严重': 'danger', '重要': 'warning', '一般': '' }
-  return map[level] || ''
+function formatTime(value) {
+  if (!value) return '-'
+  return String(value).replace('T', ' ').slice(5, 16)
 }
 
-function getLevelKey(level) {
-  const map = { '严重': 'severe', '重要': 'important', '一般': 'normal' }
-  return map[level] || 'normal'
-}
-
-function getStatusTagType(status) {
-  const map = { HANDLED: 'success', CLOSED: '', DRAFT: 'info', PENDING: 'warning', RECEIVED: 'warning', PROCESSING: 'warning' }
-  return map[status] || 'info'
-}
-
-// ==================== 搜索功能 ====================
-const searchKey = ref('')
-const searchResults = ref([])
-
-function onSearch() {
-  if (!searchKey.value.trim()) {
-    searchResults.value = []
-    return
-  }
-  const key = searchKey.value.trim().toLowerCase()
-  searchResults.value = warningList.value.filter(v =>
-    v.name.toLowerCase().includes(key) || v.location.toLowerCase().includes(key)
-  )
-}
-
-function locatePoint(item) {
-  if (mapInstance) {
-    mapInstance.centerAndZoom(new BMapGL.Point(item.lng, item.lat), 17)
-    openDetail(item)
-  }
-  searchResults.value = []
-  searchKey.value = ''
-}
-
-// ==================== 地图相关 ====================
+// ==================== 地图 ====================
 let mapInstance = null
 const markerMap = new Map()
 
@@ -244,22 +258,13 @@ function initMap() {
     console.warn('BMapGL 未加载或容器不存在')
     return
   }
-
   try {
     mapInstance = new BMapGL.Map('lsyjGISMap')
-    const centerPoint = new BMapGL.Point(110.393, 28.452)
-    mapInstance.centerAndZoom(centerPoint, 15)
+    mapInstance.centerAndZoom(new BMapGL.Point(110.393, 28.452), 15)
     mapInstance.enableScrollWheelZoom(true)
 
-    warningList.value.forEach(point => {
-      addMapMarker(point)
-    })
-
-    // 默认定位到第一个预警点位
-    if (warningList.value.length > 0) {
-      const firstPoint = warningList.value[0]
-      mapInstance.centerAndZoom(new BMapGL.Point(firstPoint.lng, firstPoint.lat), 15)
-    }
+    mapPoints.value.forEach(addMapMarker)
+    fitView()
   } catch (e) {
     console.error('百度地图初始化失败:', e)
   }
@@ -268,7 +273,6 @@ function initMap() {
 function addMapMarker(point) {
   const color = getLevelColor(point.level)
   const bPoint = new BMapGL.Point(point.lng, point.lat)
-
   const html = `<div style="text-align:center;cursor:pointer;">
     <div style="color:#fff;background:${color};border-radius:4px;padding:2px 8px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;box-shadow:0 1px 4px rgba(0,0,0,0.3);">
       ${point.name}
@@ -280,49 +284,48 @@ function addMapMarker(point) {
     position: bPoint,
     offset: new BMapGL.Size(-30, -50)
   })
-  label.setStyle({
-    border: 'none',
-    background: 'transparent',
-    padding: '0',
-    zIndex: '10'
-  })
+  label.setStyle({ border: 'none', background: 'transparent', padding: '0', zIndex: '10' })
   mapInstance.addOverlay(label)
   markerMap.set(point.id, label)
 
-  label.addEventListener('click', function () {
-    openDetail(point)
-  })
+  label.addEventListener('click', () => locate(point))
 }
 
-// ==================== 等级筛选 ====================
-function onLevelChange() {
-  warningList.value.forEach(point => {
-    const label = markerMap.get(point.id)
-    if (!label) return
-    const key = getLevelKey(point.level)
-    const visible = levelChecked[key]
-    label.show()
-    if (!visible) {
-      label.hide()
-    }
-  })
+/** 回到能看全所有点位的视野 */
+function fitView() {
+  if (!mapInstance || !mapPoints.value.length) return
+  const points = mapPoints.value.map((p) => new BMapGL.Point(p.lng, p.lat))
+  if (points.length === 1) {
+    mapInstance.centerAndZoom(points[0], 16)
+    return
+  }
+  mapInstance.setViewport(points)
 }
 
-// ==================== 详情弹窗 ====================
+function locate(item) {
+  if (!item) return
+  if (mapInstance && item.lng && item.lat) {
+    mapInstance.centerAndZoom(new BMapGL.Point(item.lng, item.lat), 17)
+  }
+  if (!item.lng || !item.lat) {
+    ElMessage.warning('该预警缺少经纬度坐标,无法在地图上定位')
+  }
+  openDetail(item)
+}
+
+// ==================== 详情 ====================
 const detailVisible = ref(false)
-const currentItem = ref({})
+const currentRecord = ref(null)
 
-function openDetail(point) {
-  currentItem.value = { ...point }
+function openDetail(item) {
+  currentRecord.value = item.raw || null
   detailVisible.value = true
 }
 
 // ==================== 生命周期 ====================
 onMounted(async () => {
   await loadData()
-  nextTick(() => {
-    initMap()
-  })
+  nextTick(() => initMap())
 })
 </script>
 
@@ -344,28 +347,23 @@ onMounted(async () => {
   position: absolute;
   top: 16px;
   left: 16px;
-  width: 280px;
-  background: rgba(6, 30, 65, 0.85);
-  border-radius: 8px;
+  bottom: 16px;
+  width: 300px;
+  display: flex;
+  flex-direction: column;
+  background: rgba(6, 30, 65, 0.88);
+  border: 1px solid rgba(90, 160, 255, 0.18);
+  border-radius: 10px;
   z-index: 100;
-  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
-  padding: 0;
-  max-height: calc(100% - 32px);
-  overflow-y: auto;
-}
-
-.side-panel::-webkit-scrollbar {
-  width: 4px;
-}
-
-.side-panel::-webkit-scrollbar-thumb {
-  background: rgba(255, 255, 255, 0.2);
-  border-radius: 2px;
+  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
+  backdrop-filter: blur(6px);
+  overflow: hidden;
 }
 
 .panel-header {
   padding: 16px 18px 12px;
   border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+  flex-shrink: 0;
 }
 
 .panel-title {
@@ -375,88 +373,103 @@ onMounted(async () => {
   letter-spacing: 1px;
 }
 
+.panel-sub {
+  margin-top: 6px;
+  font-size: 12px;
+  color: rgba(255, 255, 255, 0.55);
+}
+
+.panel-sub b {
+  color: #61c0ff;
+  font-weight: 600;
+}
+
 .panel-section {
-  padding: 14px 18px;
+  padding: 12px 18px;
   border-bottom: 1px solid rgba(255, 255, 255, 0.06);
+  flex-shrink: 0;
 }
 
-.panel-section:last-child {
+.panel-section.list-section {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
   border-bottom: none;
+  padding-bottom: 14px;
 }
 
 .section-label {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
   font-size: 12px;
-  color: rgba(255, 255, 255, 0.5);
+  color: rgba(255, 255, 255, 0.55);
   margin-bottom: 10px;
-  text-transform: uppercase;
   letter-spacing: 1px;
 }
 
-/* 等级列表 */
-.layer-list {
-  display: flex;
-  flex-direction: column;
-  gap: 6px;
-}
-
-.layer-item :deep(.el-checkbox) {
-  color: rgba(255, 255, 255, 0.85);
-  height: auto;
-}
-
-.layer-item :deep(.el-checkbox__label) {
-  color: rgba(255, 255, 255, 0.85);
-  font-size: 13px;
-  display: inline-flex;
-  align-items: center;
-  gap: 4px;
+.section-hint {
+  font-size: 11px;
+  color: rgba(255, 255, 255, 0.32);
+  letter-spacing: 0;
 }
 
-.layer-item :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
-  background: #409eff;
-  border-color: #409eff;
+.link-btn {
+  font-size: 12px;
+  color: #61c0ff;
+  cursor: pointer;
+  letter-spacing: 0;
 }
 
-.level-dot {
-  display: inline-block;
-  width: 10px;
-  height: 10px;
-  border-radius: 50%;
-  margin-right: 2px;
-  vertical-align: middle;
+.link-btn:hover {
+  text-decoration: underline;
 }
 
 /* 统计卡片 */
-.stats-row {
-  display: flex;
-  gap: 8px;
+.stat-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 6px;
 }
 
 .stat-card {
-  flex: 1;
-  background: rgba(255, 255, 255, 0.08);
+  background: rgba(255, 255, 255, 0.07);
+  border: 1px solid transparent;
   border-radius: 6px;
-  padding: 10px 4px;
+  padding: 9px 2px;
   text-align: center;
+  cursor: pointer;
+  transition: all 0.18s;
+}
+
+.stat-card:hover {
+  background: rgba(255, 255, 255, 0.13);
+}
+
+.stat-card.active {
+  border-color: rgba(97, 192, 255, 0.85);
+  background: rgba(97, 192, 255, 0.16);
 }
 
 .stat-card.stat-severe {
-  background: rgba(245, 108, 108, 0.15);
+  background: rgba(245, 108, 108, 0.14);
 }
 
 .stat-card.stat-important {
-  background: rgba(230, 162, 60, 0.15);
+  background: rgba(230, 162, 60, 0.14);
 }
 
 .stat-card.stat-normal {
-  background: rgba(64, 158, 255, 0.15);
+  background: rgba(64, 158, 255, 0.14);
 }
 
 .stat-value {
-  font-size: 20px;
+  font-size: 19px;
   font-weight: 700;
   color: #fff;
   line-height: 1.2;
+  font-variant-numeric: tabular-nums;
 }
 
 .stat-severe .stat-value {
@@ -474,17 +487,25 @@ onMounted(async () => {
 .stat-text {
   font-size: 11px;
   color: rgba(255, 255, 255, 0.5);
-  margin-top: 4px;
+  margin-top: 3px;
 }
 
-/* 搜索 */
-.side-panel :deep(.el-input__wrapper) {
+/* 筛选行 */
+.filter-row {
+  display: flex;
+  gap: 6px;
+}
+
+/* 深色面板下的输入控件 */
+.side-panel :deep(.el-input__wrapper),
+.side-panel :deep(.el-select__wrapper) {
   background: rgba(255, 255, 255, 0.08);
   box-shadow: none;
   border: 1px solid rgba(255, 255, 255, 0.12);
 }
 
-.side-panel :deep(.el-input__inner) {
+.side-panel :deep(.el-input__inner),
+.side-panel :deep(.el-select__placeholder) {
   color: #fff;
   font-size: 13px;
 }
@@ -493,144 +514,150 @@ onMounted(async () => {
   color: rgba(255, 255, 255, 0.35);
 }
 
-.side-panel :deep(.el-input__prefix .el-icon) {
-  color: rgba(255, 255, 255, 0.4);
+.side-panel :deep(.el-input__prefix .el-icon),
+.side-panel :deep(.el-select__caret) {
+  color: rgba(255, 255, 255, 0.45);
 }
 
-.search-results {
+/* 预警清单 */
+.warning-list {
+  flex: 1;
+  min-height: 0;
   margin-top: 8px;
-  max-height: 200px;
   overflow-y: auto;
-  border-radius: 4px;
+  margin-right: -6px;
+  padding-right: 4px;
 }
 
-.search-results::-webkit-scrollbar {
-  width: 3px;
+.warning-list::-webkit-scrollbar {
+  width: 4px;
 }
 
-.search-results::-webkit-scrollbar-thumb {
-  background: rgba(255, 255, 255, 0.15);
+.warning-list::-webkit-scrollbar-thumb {
+  background: rgba(255, 255, 255, 0.18);
   border-radius: 2px;
 }
 
-.search-item {
+.warning-item {
   display: flex;
   align-items: center;
-  padding: 7px 10px;
+  gap: 8px;
+  padding: 8px 8px;
+  border-radius: 6px;
   cursor: pointer;
-  border-radius: 4px;
   transition: background 0.15s;
 }
 
-.search-item:hover {
+.warning-item:hover {
   background: rgba(255, 255, 255, 0.1);
 }
 
-.search-name {
-  flex: 1;
-  font-size: 12px;
-  color: rgba(255, 255, 255, 0.85);
-  margin-left: 6px;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
+/* 无坐标的记录仍可查看详情,视觉上弱化 */
+.warning-item.no-coord .wi-name,
+.warning-item.no-coord .wi-sub {
+  color: rgba(255, 255, 255, 0.42);
 }
 
-.search-level {
-  font-size: 11px;
-  padding: 1px 6px;
-  border-radius: 3px;
+.warning-item.no-coord .level-dot {
+  opacity: 0.4;
 }
 
-.search-level.level-severe {
-  color: #f56c6c;
-  background: rgba(245, 108, 108, 0.15);
-}
-
-.search-level.level-important {
-  color: #e6a23c;
-  background: rgba(230, 162, 60, 0.15);
+.wi-main {
+  flex: 1;
+  min-width: 0;
 }
 
-.search-level.level-normal {
-  color: #409eff;
-  background: rgba(64, 158, 255, 0.15);
+.wi-name {
+  font-size: 13px;
+  color: rgba(255, 255, 255, 0.9);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
-/* ==================== 详情弹窗样式 ==================== */
-.detail-dialog :deep(.el-dialog) {
-  border-radius: 10px;
+.wi-sub {
+  font-size: 11px;
+  color: rgba(255, 255, 255, 0.42);
+  margin-top: 2px;
   overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
-.detail-dialog :deep(.el-dialog__header) {
-  background: #1a1a2e;
-  padding: 14px 20px;
-  margin: 0;
+.wi-right {
+  flex-shrink: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  gap: 3px;
 }
 
-.detail-dialog :deep(.el-dialog__title) {
-  color: #fff;
-  font-size: 15px;
+.wi-level {
+  font-size: 11px;
+  padding: 1px 6px;
+  border-radius: 3px;
 }
 
-.detail-dialog :deep(.el-dialog__headerbtn .el-dialog__close) {
-  color: rgba(255, 255, 255, 0.6);
+.wi-status {
+  font-size: 10px;
+  color: rgba(255, 255, 255, 0.45);
 }
 
-.detail-dialog :deep(.el-dialog__body) {
-  padding: 0;
+.wi-status.st-handled {
+  color: #67c23a;
 }
 
-.detail-content {
-  padding: 20px;
+.wi-status.st-closed {
+  color: #61c0ff;
 }
 
-.detail-header {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  margin-bottom: 16px;
-  padding-bottom: 12px;
-  border-bottom: 1px solid #f0f0f0;
+.list-empty {
+  text-align: center;
+  font-size: 12px;
+  color: rgba(255, 255, 255, 0.32);
+  padding: 24px 0;
 }
 
-.detail-name {
-  font-size: 16px;
-  font-weight: 600;
-  color: #303133;
+.level-dot {
+  flex-shrink: 0;
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
 }
 
-.detail-info {
-  display: flex;
-  flex-direction: column;
+.wi-level.level-severe {
+  color: #f56c6c;
+  background: rgba(245, 108, 108, 0.15);
 }
 
-.info-row {
-  display: flex;
-  align-items: center;
-  padding: 8px 0;
-  border-bottom: 1px solid #f5f5f5;
+.wi-level.level-important {
+  color: #e6a23c;
+  background: rgba(230, 162, 60, 0.15);
 }
 
-.info-row:last-child {
-  border-bottom: none;
+.wi-level.level-normal {
+  color: #409eff;
+  background: rgba(64, 158, 255, 0.15);
 }
 
-.info-label {
-  width: 80px;
-  flex-shrink: 0;
-  font-size: 13px;
-  color: #909399;
+/* ==================== 地图工具条 ==================== */
+.map-tools {
+  position: absolute;
+  top: 16px;
+  right: 16px;
+  z-index: 100;
 }
 
-.info-value {
-  font-size: 13px;
-  color: #303133;
+.map-tools :deep(.el-button) {
+  background: rgba(6, 30, 65, 0.85);
+  border-color: rgba(90, 160, 255, 0.25);
+  color: #fff;
 }
 
-.info-value.highlight {
-  color: #f56c6c;
-  font-weight: 600;
+.map-tools :deep(.el-button:hover) {
+  background: rgba(30, 70, 130, 0.95);
+  border-color: rgba(90, 160, 255, 0.5);
+  color: #fff;
 }
 </style>

+ 632 - 191
src/views/subSystem/drainage/jcyj/lsyjgl.vue

@@ -1,142 +1,226 @@
 <template>
   <div class="app-container lsyjgl-page">
 
-    <!-- ==================== 搜索区域 ==================== -->
-    <div class="search-panel">
-      <div class="search-form">
-        <div class="form-item">
-          <span class="form-label">预警名称:</span>
-          <el-input v-model="filters.equipmentName" placeholder="请输入" clearable style="width:200px" />
-        </div>
-        <div class="form-item">
-          <span class="form-label">预警级别:</span>
-          <el-select v-model="filters.warningLevel" placeholder="请选择" clearable style="width:160px">
-            <el-option label="严重" :value="1" />
-            <el-option label="重要" :value="2" />
-            <el-option label="一般" :value="3" />
-          </el-select>
+    <!-- ==================== 分类统计 ==================== -->
+    <div class="stats-panel">
+      <div class="stats-left">
+        <div class="stat-item" v-for="item in statItems" :key="item.label">
+          <div class="stat-value" :class="item.cls">{{ item.value }}</div>
+          <div class="stat-label">{{ item.label }}</div>
         </div>
-        <div class="form-item">
-          <span class="form-label">处理状态:</span>
-          <el-select v-model="filters.status" placeholder="请选择" clearable style="width:160px">
-            <el-option label="已处置" value="HANDLED" />
-            <el-option label="已解除" value="CLOSED" />
-          </el-select>
+      </div>
+      <div class="stats-right">
+        <div class="ring-chart-box" v-for="chart in ringCharts" :key="chart.id">
+          <div :id="chart.id" class="ring-chart"></div>
+          <div class="ring-legend">
+            <div class="legend-row" v-for="(leg, i) in chart.legends.value" :key="i">
+              <span class="legend-dot" :style="{ background: leg.color }"></span>
+              <span class="legend-text">{{ leg.name }}</span>
+              <span class="legend-num">{{ leg.value }}</span>
+            </div>
+          </div>
         </div>
-        <el-button type="primary" @click="handleSearch">搜索</el-button>
-        <el-button @click="handleClear">清除</el-button>
       </div>
     </div>
 
+    <!-- ==================== 搜索区域 ==================== -->
+    <el-form ref="queryRef" :model="filters" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item label="预警名称">
+        <el-input v-model="filters.equipmentName" placeholder="请输入预警名称" clearable style="width: 200px"
+          @keyup.enter="handleSearch" />
+      </el-form-item>
+      <el-form-item label="预警类型">
+        <el-select v-model="filters.warningType" placeholder="请选择预警类型" clearable filterable style="width: 180px">
+          <el-option v-for="item in warningTypeOptions" :key="item.dictValue" :label="item.dictLabel"
+            :value="item.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="预警级别">
+        <el-select v-model="filters.warningLevel" placeholder="请选择预警级别" clearable style="width: 180px">
+          <el-option label="严重" :value="1" />
+          <el-option label="重要" :value="2" />
+          <el-option label="一般" :value="3" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="处理状态">
+        <el-select v-model="filters.status" placeholder="请选择处理状态" clearable style="width: 180px">
+          <el-option label="已处置" value="HANDLED" />
+          <el-option label="已解除" value="CLOSED" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="预警时间" style="width: 308px">
+        <el-date-picker v-model="filters.dateRange" type="daterange" range-separator="-" start-placeholder="开始日期"
+          end-placeholder="结束日期" value-format="YYYY-MM-DD" style="width: 240px" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
+        <el-button icon="Refresh" @click="handleClear">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- ==================== 操作栏 ==================== -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="Download" :loading="exporting" @click="handleExport">导出</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="Refresh" :loading="loading" @click="loadData">刷新</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="loadData"></right-toolbar>
+    </el-row>
+
     <!-- ==================== 数据表格 ==================== -->
-    <div class="table-panel">
-      <el-table :data="tableData" border style="width:100%" v-loading="loading">
-        <el-table-column prop="warningNo" label="预警编号" min-width="140" />
-        <el-table-column prop="equipmentName" label="预警名称" min-width="140" show-overflow-tooltip />
+    <div>
+      <el-table :data="tableData" style="width:100%" v-loading="loading"
+        empty-text="暂无已处置 / 已解除的预警记录">
+        <el-table-column label="归档编号" width="150" show-overflow-tooltip>
+          <template #default="{ row }">{{ archiveNoOf(row) }}</template>
+        </el-table-column>
+        <el-table-column prop="warningNo" label="预警编号" width="175" show-overflow-tooltip />
+        <el-table-column prop="equipmentName" label="预警名称" min-width="130" show-overflow-tooltip />
+        <el-table-column label="预警类型" width="130" align="center" show-overflow-tooltip>
+          <template #default="{ row }">{{ warningTypeText(row) }}</template>
+        </el-table-column>
         <el-table-column prop="warningLevel" label="预警级别" width="90" align="center">
           <template #default="{ row }">
             <el-tag :type="levelTagType(row.warningLevel)" size="small">{{ levelText(row.warningLevel) }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="deviceCode" label="预警类型" width="110" align="center">
-          <template #default="{ row }">
-            {{ equipmentTypeMap.get(String(row.deviceCode)) || row.deviceCode || row.warningType || '-' }}
-          </template>
+        <el-table-column label="预警内容" min-width="190" show-overflow-tooltip>
+          <template #default="{ row }">{{ row.content || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="ownershipUnit" label="权属单位" width="145" show-overflow-tooltip>
+          <template #default="{ row }">{{ row.ownershipUnit || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="location" label="位置" min-width="140" show-overflow-tooltip>
+          <template #default="{ row }">{{ row.location || '-' }}</template>
         </el-table-column>
-        <el-table-column prop="warningSpecial" label="预警专项" width="110" align="center" show-overflow-tooltip />
-        <el-table-column prop="location" label="位置" min-width="180" show-overflow-tooltip />
-        <el-table-column prop="createTime" label="预警时间" width="160" />
-        <el-table-column prop="status" label="处理状态" width="90" align="center">
+        <el-table-column label="预警时间" width="168">
+          <template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
+        </el-table-column>
+        <el-table-column prop="publisher" label="发布人" width="110" show-overflow-tooltip>
+          <template #default="{ row }">{{ row.publisher || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="processor" label="处置人" width="110" show-overflow-tooltip>
+          <template #default="{ row }">{{ row.processor || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="归档时间" width="168">
+          <template #default="{ row }">{{ formatDateTime(archiveTimeOf(row)) }}</template>
+        </el-table-column>
+        <el-table-column prop="status" label="处理状态" width="100" align="center">
           <template #default="{ row }">
             <el-tag :type="statusTagType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="140" fixed="right">
+        <el-table-column label="操作" width="130" fixed="right">
           <template #default="{ row }">
-            <el-button link type="primary" @click="handleView(row)">查看</el-button>
-            <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
+            <el-button link type="primary" size="small" @click="handleView(row)">查看</el-button>
+            <el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
-      <el-pagination
-        v-model:current-page="pageNum"
-        v-model:page-size="pageSize"
-        :page-sizes="[10, 20, 50, 100]"
-        :total="total"
-        layout="total, sizes, prev, pager, next, jumper"
-        style="margin-top: 16px; justify-content: flex-end"
-        @size-change="loadData"
-        @current-change="loadData"
-      />
+      <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize"
+        @pagination="loadData" />
     </div>
 
     <!-- ==================== 查看详情弹窗 ==================== -->
-    <el-drawer
-      v-model="dialogVisible"
-      title="历史预警详情"
-      size="680px"
-      :close-on-click-modal="false"
-     class="detail-drawer" direction="rtl">
-      <div class="detail-body" v-if="currentDetail">
-        <!-- 右侧信息 -->
-        <div class="detail-info">
-          <div class="info-code">预警编号:{{ currentDetail.warningNo }}</div>
-          <div class="info-grid">
-            <div class="info-row">
-              <div class="info-item">
-                <span class="info-label">预警名称:</span>
-                <span class="info-value">{{ currentDetail.equipmentName }}</span>
-              </div>
-              <div class="info-item">
-                <span class="info-label">预警级别:</span>
-                <span class="info-value">
-                  <el-tag :type="levelTagType(currentDetail.warningLevel)" size="small">{{ levelText(currentDetail.warningLevel) }}</el-tag>
-                </span>
-              </div>
-            </div>
-            <div class="info-row">
-              <div class="info-item">
-                <span class="info-label">预警类型:</span>
-                <span class="info-value">{{ equipmentTypeMap.get(String(currentDetail.deviceCode)) || currentDetail.deviceCode || currentDetail.warningType || '-' }}</span>
-              </div>
-              <div class="info-item">
-                <span class="info-label">预警专项:</span>
-                <span class="info-value">{{ currentDetail.warningSpecial || '-' }}</span>
-              </div>
-            </div>
-            <div class="info-row">
-              <div class="info-item">
-                <span class="info-label">预警时间:</span>
-                <span class="info-value">{{ currentDetail.createTime }}</span>
-              </div>
-              <div class="info-item">
-                <span class="info-label">发布人:</span>
-                <span class="info-value">{{ currentDetail.publisher || '-' }}</span>
+    <el-drawer v-model="dialogVisible" title="历史预警详情" size="720px" :close-on-click-modal="false"
+      class="detail-drawer" direction="rtl">
+      <div class="detail-content" v-if="currentDetail" v-loading="detailLoading">
+        <div class="section-box">
+          <div class="section-title">预警信息</div>
+          <el-descriptions :column="2" border size="small">
+            <el-descriptions-item label="预警编号">{{ currentDetail.warningNo || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="预警名称">{{ currentDetail.equipmentName || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="预警类型">{{ warningTypeText(currentDetail) }}</el-descriptions-item>
+            <el-descriptions-item label="预警级别">
+              <el-tag :type="levelTagType(currentDetail.warningLevel)" size="small">{{
+                levelText(currentDetail.warningLevel) }}</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="预警专项">{{ currentDetail.warningSpecial || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="权属单位">{{ currentDetail.ownershipUnit || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="预警时间">{{ formatDateTime(currentDetail.createTime) }}</el-descriptions-item>
+            <el-descriptions-item label="处理状态">
+              <el-tag :type="statusTagType(currentDetail.status)" size="small">{{ statusText(currentDetail.status)
+                }}</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="发布人">{{ currentDetail.publisher || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="创建人">{{ currentDetail.createBy || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="位置" :span="2">{{ currentDetail.location || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="预警内容" :span="2">
+              <div class="desc-text">{{ currentDetail.content || '-' }}</div>
+            </el-descriptions-item>
+            <el-descriptions-item v-if="currentDetail.remark" label="备注" :span="2">
+              {{ currentDetail.remark }}
+            </el-descriptions-item>
+          </el-descriptions>
+        </div>
+
+        <div class="section-box">
+          <div class="section-title">归档信息</div>
+          <el-descriptions :column="2" border size="small">
+            <el-descriptions-item label="归档编号">{{ archiveNoOf(currentDetail) }}</el-descriptions-item>
+            <el-descriptions-item label="归档时间">{{ formatDateTime(archiveTimeOf(currentDetail)) }}</el-descriptions-item>
+            <el-descriptions-item label="处置人">{{ currentDetail.processor || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="归档状态">{{ statusText(currentDetail.status) }}</el-descriptions-item>
+            <el-descriptions-item label="发布时间" :span="2">
+              {{ formatDateTime(currentDetail.publishTime) }}
+            </el-descriptions-item>
+          </el-descriptions>
+        </div>
+
+        <div class="section-box">
+          <div class="section-title">关联设备与预警指标</div>
+          <el-descriptions v-if="detailDevice" :column="2" border size="small">
+            <el-descriptions-item label="设备名称">{{ detailDevice.equipmentName || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="设备编码">{{ detailDevice.equipmentCode || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="设备类型">{{ equipmentTypeMap.get(String(detailDevice.equipmentTypeId)) || '-'
+              }}</el-descriptions-item>
+            <el-descriptions-item label="在线状态">
+              <el-tag :type="detailDevice.onlineStatus === 1 ? 'success' : 'info'" size="small">
+                {{ detailDevice.onlineStatus === 1 ? '在线' : '离线' }}
+              </el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="安装位置" :span="2">{{ detailDevice.equipmentLocation ||
+              currentDetail.location || '-' }}</el-descriptions-item>
+          </el-descriptions>
+          <el-alert v-else type="info" :closable="false" show-icon class="device-missing"
+            title="未在设备台账中找到该预警关联的设备,指标数据无法展示" />
+
+          <div class="metric-grid">
+            <div class="metric-card">
+              <div class="metric-label">实时液位</div>
+              <div class="metric-value">
+                <template v-if="detailMetric.level !== null">{{ detailMetric.level
+                  }}<span class="metric-unit">m</span></template>
+                <span v-else class="metric-empty">—</span>
               </div>
             </div>
-            <div class="info-row">
-              <div class="info-item">
-                <span class="info-label">处理状态:</span>
-                <span class="info-value">
-                  <el-tag :type="statusTagType(currentDetail.status)" size="small">{{ statusText(currentDetail.status) }}</el-tag>
-                </span>
-              </div>
-              <div class="info-item">
-                <span class="info-label">发布时间:</span>
-                <span class="info-value">{{ currentDetail.publishTime || '-' }}</span>
-              </div>
+            <div class="metric-card">
+              <div class="metric-label">报警阈值</div>
+              <div class="metric-value metric-value-text">{{ detailMetric.thresholdText || '未配置' }}</div>
             </div>
-            <div class="info-row single">
-              <div class="info-item">
-                <span class="info-label">位置:</span>
-                <span class="info-value desc-text">{{ currentDetail.location }}</span>
-              </div>
+            <div class="metric-card">
+              <div class="metric-label">采集时间</div>
+              <div class="metric-value metric-value-text">{{ detailMetric.collectTime || '—' }}</div>
             </div>
           </div>
+
+          <el-table v-if="detailThresholds.length" :data="detailThresholds" border size="small"
+            empty-text="该设备尚未配置报警阈值">
+            <el-table-column label="监测指标" prop="warningName" min-width="120" />
+            <el-table-column label="单位" width="90" align="center">
+              <template #default="{ row }">{{ row.warningUnit || '-' }}</template>
+            </el-table-column>
+            <el-table-column label="阈值范围" min-width="150" align="center">
+              <template #default="{ row }">{{ formatThreshold(row) }}</template>
+            </el-table-column>
+          </el-table>
         </div>
       </div>
+
       <template #footer>
-        <el-button @click="dialogVisible = false">关闭</el-button>
+        <el-button @click="dialogVisible = false">关 闭</el-button>
       </template>
     </el-drawer>
 
@@ -144,12 +228,21 @@
 </template>
 
 <script setup name="Lsyjgl">
-import { ref, reactive, onMounted } from 'vue'
+import { ref, reactive, computed, onMounted, nextTick } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { getWarningHistoryPage, deleteWarning, getEquipmentTypeList } from '@/api/drainage'
-
-// ==================== 设备类型映射 ====================
+import * as XLSX from 'xlsx'
+import { saveAs } from 'file-saver'
+import * as echarts from 'echarts'
+import { getWarningHistoryPage, deleteWarning, getEquipmentTypeList, getLatestWaterLevelByDevice } from '@/api/drainage'
+import { listEquipment } from '@/api/pipeNetwork/pdeviceInform'
+import { pageWarningThresholds } from '@/api/warningThreshold'
+import { listWarningTypes } from '@/api/warning/list'
+
+// ==================== 设备类型 / 预警类型映射 ====================
 const equipmentTypeMap = ref(new Map())
+/** warningType(类型编码) -> 类型名称,来自 warning_type 表 */
+const warningTypeMap = ref(new Map())
+const warningTypeOptions = ref([])
 
 async function loadEquipmentTypes() {
   try {
@@ -167,6 +260,33 @@ async function loadEquipmentTypes() {
   }
 }
 
+async function loadWarningTypes() {
+  try {
+    const res = await listWarningTypes()
+    const list = res?.data || res?.rows || []
+    const map = new Map()
+    if (Array.isArray(list)) {
+      list.forEach(item => {
+        if (item.dictValue) map.set(String(item.dictValue), item.dictLabel || item.dictValue)
+      })
+    }
+    warningTypeOptions.value = Array.isArray(list) ? list : []
+    warningTypeMap.value = map
+  } catch (error) {
+    console.warn('加载预警类型配置失败', error)
+  }
+}
+
+/** 预警类型展示:新数据看 warningType,历史数据回退到 deviceCode 的设备类型名 */
+function warningTypeText(row) {
+  if (!row) return '-'
+  const byType = row.warningType ? warningTypeMap.value.get(String(row.warningType)) : null
+  if (byType) return byType
+  const byDeviceType = row.deviceCode ? equipmentTypeMap.value.get(String(row.deviceCode)) : null
+  if (byDeviceType) return byDeviceType
+  return row.warningType || row.deviceCode || '-'
+}
+
 // ==================== 等级/状态工具方法 ====================
 function levelText(level) {
   const map = { 1: '严重', 2: '重要', 3: '一般' }
@@ -185,6 +305,75 @@ function statusTagType(status) {
   return map[status] || 'info'
 }
 
+function formatDateTime(value) {
+  if (!value) return '-'
+  return String(value).replace('T', ' ').slice(0, 19)
+}
+
+/** 归档编号:预警解除即归档,编号由预警编号派生,稳定可追溯 */
+function archiveNoOf(row) {
+  if (!row?.warningNo) return '-'
+  return `ARC${String(row.warningNo).replace(/^WRN/, '').slice(-12)}`
+}
+
+/** 归档时间:解除/处置动作发生在 update_time,缺失时退回发布时间 */
+function archiveTimeOf(row) {
+  return row?.updateTime || row?.publishTime || null
+}
+
+// ==================== 分类统计 ====================
+const statItems = computed(() => {
+  const list = tableData.value
+  const devices = new Set(list.map(o => o.equipmentName).filter(Boolean))
+  return [
+    { label: '归档预警', value: total.value, cls: '' },
+    { label: '已处置', value: list.filter(o => o.status === 'HANDLED').length, cls: 'green' },
+    { label: '已解除', value: list.filter(o => o.status === 'CLOSED').length, cls: 'blue' },
+    { label: '涉及设备', value: devices.size, cls: '' }
+  ]
+})
+
+const statusLegends = computed(() => [
+  { name: '已处置', value: tableData.value.filter(o => o.status === 'HANDLED').length, color: '#67C23A' },
+  { name: '已解除', value: tableData.value.filter(o => o.status === 'CLOSED').length, color: '#409EFF' }
+])
+
+const levelLegends = computed(() => [
+  { name: '严重', value: tableData.value.filter(o => o.warningLevel === 1).length, color: '#F56C6C' },
+  { name: '重要', value: tableData.value.filter(o => o.warningLevel === 2).length, color: '#E6A23C' },
+  { name: '一般', value: tableData.value.filter(o => o.warningLevel === 3).length, color: '#909399' }
+])
+
+const ringCharts = [
+  { id: 'historyStatusChart', legends: statusLegends },
+  { id: 'historyLevelChart', legends: levelLegends }
+]
+
+function initCharts() {
+  ringCharts.forEach(chart => {
+    const dom = document.getElementById(chart.id)
+    if (!dom) return
+    const legends = chart.legends.value
+    const chartInstance = echarts.getInstanceByDom(dom) || echarts.init(dom)
+    chartInstance.setOption({
+      tooltip: { trigger: 'item', formatter: '{b}:{c}({d}%)' },
+      series: [{
+        type: 'pie',
+        radius: ['58%', '82%'],
+        center: ['50%', '50%'],
+        avoidLabelOverlap: false,
+        label: { show: false },
+        labelLine: { show: false },
+        data: legends.map(item => ({
+          name: item.name,
+          value: item.value,
+          itemStyle: { color: item.color }
+        }))
+      }]
+    }, true)
+  })
+}
+
 // ==================== 表格数据 ====================
 const tableData = ref([])
 const total = ref(0)
@@ -193,26 +382,38 @@ const pageNum = ref(1)
 const pageSize = ref(10)
 
 // ==================== 搜索筛选 ====================
+const showSearch = ref(true)
+const queryRef = ref()
 const filters = reactive({
   equipmentName: '',
+  warningType: '',
   warningLevel: '',
-  status: ''
+  status: '',
+  dateRange: null
 })
 
+function buildParams() {
+  const params = {
+    historyOnly: true,
+    equipmentType: 'drainage'
+  }
+  if (filters.equipmentName) params.equipmentName = filters.equipmentName
+  if (filters.warningType) params.warningType = filters.warningType
+  if (filters.warningLevel) params.warningLevel = filters.warningLevel
+  if (filters.status !== '' && filters.status !== null) params.status = filters.status
+  if (filters.dateRange && filters.dateRange.length === 2) {
+    params.startTime = `${filters.dateRange[0]} 00:00:00`
+    params.endTime = `${filters.dateRange[1]} 23:59:59`
+  }
+  return params
+}
+
 // ==================== 数据加载 ====================
 async function loadData() {
   loading.value = true
   try {
-    // 历史页由后端按历史状态(HANDLED/CLOSED)限定;不再写死
-    // 旧的数字类型和 HANDLED 状态,避免过滤掉实际历史记录。
-    const params = {
-      historyOnly: true,
-      equipmentType: 'drainage'
-    }
-    if (filters.equipmentName) params.equipmentName = filters.equipmentName
-    if (filters.warningLevel) params.warningLevel = filters.warningLevel
-    if (filters.status !== '' && filters.status !== null) params.status = filters.status
-    const res = await getWarningHistoryPage(pageNum.value, pageSize.value, params)
+    // 历史页由后端按历史状态(HANDLED/CLOSED)限定
+    const res = await getWarningHistoryPage(pageNum.value, pageSize.value, buildParams())
     const pageData = res.code !== undefined ? res.data : res
     tableData.value = pageData.records || []
     total.value = pageData.total || 0
@@ -221,6 +422,7 @@ async function loadData() {
     ElMessage.error('加载历史预警列表失败')
   } finally {
     loading.value = false
+    nextTick(() => initCharts())
   }
 }
 
@@ -231,12 +433,84 @@ function handleSearch() {
 
 function handleClear() {
   filters.equipmentName = ''
+  filters.warningType = ''
   filters.warningLevel = ''
   filters.status = ''
+  filters.dateRange = null
   pageNum.value = 1
   loadData()
 }
 
+// ==================== 归档导出 ====================
+const exporting = ref(false)
+const EXPORT_PAGE_SIZE = 100
+
+const EXPORT_HEADER = [
+  '归档编号', '预警编号', '预警名称', '预警类型', '预警级别', '预警内容',
+  '权属单位', '位置', '预警时间', '发布人', '处置人', '归档时间', '处理状态'
+]
+
+function exportValues(row) {
+  return [
+    archiveNoOf(row),
+    row.warningNo || '',
+    row.equipmentName || '',
+    warningTypeText(row),
+    levelText(row.warningLevel),
+    row.content || '',
+    row.ownershipUnit || '',
+    row.location || '',
+    formatDateTime(row.createTime),
+    row.publisher || '',
+    row.processor || '',
+    formatDateTime(archiveTimeOf(row)),
+    statusText(row.status)
+  ]
+}
+
+/** 按当前筛选条件翻页拉全量,单页给大值容易被后端分页上限截断 */
+async function fetchAllRows() {
+  const collected = []
+  let page = 1
+  while (page <= 50) {
+    const res = await getWarningHistoryPage(page, EXPORT_PAGE_SIZE, buildParams())
+    const pageData = res.code !== undefined ? res.data : res
+    const rows = pageData.records || pageData.rows || []
+    if (rows.length === 0) break
+    collected.push(...rows)
+    const totalCount = Number(pageData.total) || 0
+    if (rows.length < EXPORT_PAGE_SIZE) break
+    if (totalCount > 0 && collected.length >= totalCount) break
+    page += 1
+  }
+  return collected
+}
+
+async function handleExport() {
+  exporting.value = true
+  try {
+    const rows = await fetchAllRows()
+    if (rows.length === 0) {
+      ElMessage.warning('当前条件下没有可导出的归档记录')
+      return
+    }
+    const sheet = XLSX.utils.aoa_to_sheet([EXPORT_HEADER, ...rows.map(exportValues)])
+    const book = XLSX.utils.book_new()
+    XLSX.utils.book_append_sheet(book, sheet, '历史预警归档')
+    const d = new Date()
+    const pad = (n) => String(n).padStart(2, '0')
+    const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}`
+    saveAs(new Blob([XLSX.write(book, { bookType: 'xlsx', type: 'array' })]),
+      `历史预警归档清单_${stamp}.xlsx`)
+    ElMessage.success(`已导出 ${rows.length} 条归档记录`)
+  } catch (error) {
+    console.error('导出归档清单失败', error)
+    ElMessage.error('导出失败,请稍后重试')
+  } finally {
+    exporting.value = false
+  }
+}
+
 // ==================== 操作:删除 ====================
 async function handleDelete(row) {
   try {
@@ -252,14 +526,108 @@ async function handleDelete(row) {
 // ==================== 查看详情弹窗 ====================
 const dialogVisible = ref(false)
 const currentDetail = ref(null)
+const detailLoading = ref(false)
+const detailDevice = ref(null)
+const detailThresholds = ref([])
+const waterLevelMap = ref({})
+const thresholdMap = ref({})
+/** equipmentName -> 设备档案:预警 device_code 存的是类型 id,只能按名称反查设备 */
+const deviceByName = ref(null)
+
+const detailMetric = computed(() => {
+  const code = detailDevice.value?.equipmentCode
+  const levelItem = code ? waterLevelMap.value[code] : null
+  const thresholds = code ? (thresholdMap.value[code] || []) : []
+  return {
+    level: levelItem && levelItem.value !== undefined && levelItem.value !== null ? levelItem.value : null,
+    collectTime: levelItem?.time ? formatDateTime(levelItem.time) : '',
+    thresholdText: thresholds.length > 0 ? thresholds.map(formatThreshold).join(' / ') : ''
+  }
+})
 
-function handleView(row) {
+async function handleView(row) {
   currentDetail.value = { ...row }
+  detailDevice.value = null
+  detailThresholds.value = []
   dialogVisible.value = true
+  detailLoading.value = true
+  try {
+    const [device] = await Promise.all([resolveDevice(row.equipmentName), loadLiveMetrics()])
+    detailDevice.value = device || null
+    const code = device?.equipmentCode
+    detailThresholds.value = code ? (thresholdMap.value[code] || []) : []
+  } catch (error) {
+    console.warn('预警详情数据加载失败', error)
+  } finally {
+    detailLoading.value = false
+  }
+}
+
+async function resolveDevice(equipmentName) {
+  if (!equipmentName) return null
+  if (!deviceByName.value) {
+    const map = new Map()
+    let page = 1
+    while (page <= 10) {
+      const res = await listEquipment({ rootTypeId: 2, pageNum: page, pageSize: 100 })
+      const pageData = res?.data && Array.isArray(res.data.records) ? res.data : res
+      const rows = pageData?.records || pageData?.rows || []
+      if (rows.length === 0) break
+      rows.forEach((item) => {
+        if (item?.equipmentName && !map.has(item.equipmentName)) map.set(item.equipmentName, item)
+      })
+      if (rows.length < 100) break
+      page += 1
+    }
+    deviceByName.value = map
+  }
+  return deviceByName.value.get(equipmentName) || null
+}
+
+async function loadLiveMetrics() {
+  const [levelResult, thresholdResult] = await Promise.allSettled([
+    getLatestWaterLevelByDevice(),
+    pageWarningThresholds({ rootTypeId: 2, pageNum: 1, pageSize: 500 })
+  ])
+
+  if (levelResult.status === 'fulfilled') {
+    const list = Array.isArray(levelResult.value) ? levelResult.value : (levelResult.value?.data || [])
+    const map = {}
+    list.forEach((item) => {
+      if (!item?.deviceCode) return
+      map[item.deviceCode] = { value: item.waterLevel, time: item.createTime }
+    })
+    waterLevelMap.value = map
+  }
+
+  if (thresholdResult.status === 'fulfilled') {
+    const res = thresholdResult.value
+    const pageData = res?.data && Array.isArray(res.data.records) ? res.data : res
+    const rows = pageData?.records || pageData?.rows || []
+    const map = {}
+    rows.forEach((row) => {
+      if (!row?.deviceCode) return
+      if (!map[row.deviceCode]) map[row.deviceCode] = []
+      map[row.deviceCode].push(row)
+    })
+    thresholdMap.value = map
+  }
+}
+
+/** 阈值展示:dataType 0 区间 / 1 下限 / 2 上限 */
+function formatThreshold(item) {
+  if (!item) return '-'
+  const unit = item.warningUnit || ''
+  const min = item.minValue
+  const max = item.maxValue
+  if (item.dataType === 1) return min === null || min === undefined ? '-' : `≥ ${min}${unit}`
+  if (item.dataType === 2) return max === null || max === undefined ? '-' : `≤ ${max}${unit}`
+  if ((min === null || min === undefined) && (max === null || max === undefined)) return '未设置'
+  return `${min ?? '-'} ~ ${max ?? '-'}${unit}`
 }
 
 onMounted(async () => {
-  await loadEquipmentTypes()
+  await Promise.all([loadEquipmentTypes(), loadWarningTypes()])
   loadData()
 })
 </script>
@@ -271,6 +639,98 @@ onMounted(async () => {
   padding: 16px;
 }
 
+/* 分类统计 */
+.stats-panel {
+  display: flex;
+  gap: 16px;
+  align-items: stretch;
+  margin-bottom: 16px;
+  padding: 16px 20px;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+}
+
+.stats-left {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 12px;
+  width: 300px;
+  flex-shrink: 0;
+}
+
+.stat-item {
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 10px 14px;
+  background: #fafbfc;
+}
+
+.stat-value {
+  font-size: 24px;
+  font-weight: 600;
+  color: #303133;
+  line-height: 1.2;
+}
+
+.stat-value.green {
+  color: #67c23a;
+}
+
+.stat-value.blue {
+  color: #409eff;
+}
+
+.stat-label {
+  margin-top: 2px;
+  font-size: 12px;
+  color: #909399;
+}
+
+.stats-right {
+  flex: 1;
+  display: flex;
+  gap: 32px;
+  justify-content: center;
+  align-items: center;
+}
+
+.ring-chart-box {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.ring-chart {
+  width: 110px;
+  height: 110px;
+}
+
+.ring-legend {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.legend-row {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  color: #606266;
+  white-space: nowrap;
+}
+
+.legend-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+}
+
+.legend-num {
+  font-weight: 600;
+  color: #303133;
+}
+
 /* 搜索区 */
 .search-panel {
   background: #fff;
@@ -305,101 +765,82 @@ onMounted(async () => {
   border-radius: 4px;
 }
 
-/* 所属类型标签 */
-.type-tag {
-  font-weight: 500;
-  font-size: 13px;
+/* 详情弹窗 */
+.detail-content {
+  padding: 0 8px;
 }
 
-.type-pipe {
-  color: #409eff;
+.section-box {
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 16px;
+  margin-bottom: 16px;
 }
 
-.type-point {
-  color: #67c23a;
+.section-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 12px;
 }
 
-.type-well {
-  color: #e6a23c;
+.desc-text {
+  background: #f5f7fa;
+  border-radius: 4px;
+  padding: 10px;
+  font-size: 13px;
+  color: #606266;
+  line-height: 1.6;
+  max-height: 120px;
+  overflow-y: auto;
 }
 
-/* 详情弹窗 */
-.detail-body {
-  display: flex;
-  gap: 24px;
-  padding: 8px 4px;
+/* 预警指标卡片 */
+.metric-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 12px;
+  margin: 12px 0;
 }
 
-/* 左侧图片占位 */
-.detail-img {
-  flex-shrink: 0;
-  width: 150px;
-  height: 150px;
-  background: #d9ecff;
-  border: 2px solid #b3d8ff;
+.metric-card {
+  border: 1px solid #ebeef5;
   border-radius: 4px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.img-placeholder-icon {
-  font-size: 64px;
-  color: #409eff;
-  opacity: 0.7;
+  padding: 12px 14px;
+  background: #fafbfc;
 }
 
-/* 右侧详情信息 */
-.detail-info {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  gap: 10px;
+.metric-label {
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 6px;
 }
 
-.info-code {
-  font-size: 15px;
+.metric-value {
+  font-size: 22px;
   font-weight: 600;
   color: #303133;
-  padding-bottom: 6px;
-  border-bottom: 1px solid #f0f0f0;
-}
-
-.info-grid {
-  display: flex;
-  flex-direction: column;
-  gap: 8px;
-}
-
-.info-row {
-  display: flex;
-  gap: 16px;
+  line-height: 1.2;
 }
 
-.info-row.single {
-  display: block;
-}
-
-.info-item {
-  flex: 1;
-  display: flex;
-  align-items: baseline;
+.metric-value-text {
   font-size: 14px;
+  font-weight: 500;
+  word-break: break-all;
 }
 
-.info-label {
+.metric-unit {
+  font-size: 12px;
+  font-weight: 400;
   color: #909399;
-  white-space: nowrap;
-  min-width: 64px;
+  margin-left: 2px;
 }
 
-.info-value {
-  color: #303133;
+.metric-empty {
+  color: #c0c4cc;
 }
 
-.desc-text {
-  color: #606266;
-  line-height: 1.6;
-  word-break: break-all;
+.device-missing {
+  margin-bottom: 12px;
 }
 </style>

+ 2 - 2
src/views/subSystem/drainage/spjc/spfzgl.vue

@@ -4,9 +4,9 @@
       <el-tree :data="tree" node-key="id" highlight-current default-expand-all @node-click="selectNode"><template #default="{data}">{{ data.label }}({{ data.count }})</template></el-tree>
     </aside>
     <main>
-      <el-form :inline="true" @submit.prevent="search"><el-form-item label="视频设备"><el-input v-model="keyword" clearable placeholder="名称/编码" @keyup.enter="search" /></el-form-item><el-form-item><el-button type="primary" @click="search">查询</el-button><el-button @click="refresh">刷新</el-button></el-form-item></el-form>
+      <el-form :inline="true" @submit.prevent="search"><el-form-item label="视频设备"><el-input v-model="keyword" clearable placeholder="名称/编码" @keyup.enter="search" /></el-form-item><el-form-item><el-button type="primary" icon="Search" @click="search">搜索</el-button><el-button icon="Refresh" @click="refresh">刷新</el-button></el-form-item></el-form>
       <el-alert v-if="error" :title="error" type="error" :closable="false" />
-      <el-table :data="pageRows" border empty-text="当前分组暂无视频">
+      <el-table :data="pageRows" empty-text="当前分组暂无视频">
         <el-table-column prop="deviceName" label="视频设备" min-width="170" /><el-table-column prop="deviceCode" label="设备编码" min-width="180" show-overflow-tooltip />
         <el-table-column prop="districtName" label="所属片区" width="100" /><el-table-column label="易积水点" min-width="140"><template #default="{row}">{{ row.pointName || '未关联' }}</template></el-table-column><el-table-column prop="typeName" label="接入类型" width="100" />
         <el-table-column label="操作" width="210"><template #default="{row}">

+ 24 - 6
src/views/subSystem/drainage/spjc/spgngl.vue

@@ -2,12 +2,12 @@
   <div class="app-container">
     <el-form :inline="true" @submit.prevent="search">
       <el-form-item label="视频设备"><el-input v-model="query.query" clearable placeholder="设备名称/编码" @keyup.enter="search" /></el-form-item>
-      <el-form-item label="所属片区"><el-select v-model="query.district" clearable style="width:160px"><el-option v-for="d in districts" :key="d.value" :label="d.label" :value="d.value" /></el-select></el-form-item>
+      <el-form-item label="所属片区"><el-select v-model="query.district" clearable style="width:160px"><el-option v-for="d in districtOptions" :key="d.value" :label="d.label" :value="Number(d.value)" /></el-select></el-form-item>
       <el-form-item label="关联状态"><el-select v-model="query.unbound" clearable style="width:160px"><el-option label="已关联" :value="false" /><el-option label="未关联" :value="true" /></el-select></el-form-item>
-      <el-form-item><el-button type="primary" @click="search">查询</el-button><el-button @click="reset">重置</el-button></el-form-item>
+      <el-form-item><el-button type="primary" icon="Search" @click="search">搜索</el-button><el-button icon="Refresh" @click="reset">重置</el-button></el-form-item>
     </el-form>
     <el-alert v-if="error" :title="error" type="error" show-icon :closable="false" />
-    <el-table v-loading="loading" :data="rows" border empty-text="暂无排水视频设备,请先在视频设备管理中纳管">
+    <el-table v-loading="loading" :data="rows" empty-text="暂无排水视频设备,请先在视频设备管理中纳管">
       <el-table-column prop="deviceCode" label="设备编码" min-width="180" show-overflow-tooltip /><el-table-column prop="deviceName" label="设备名称" min-width="160" />
       <el-table-column prop="districtName" label="所属片区" width="100" /><el-table-column prop="location" label="安装位置" min-width="180" show-overflow-tooltip />
       <el-table-column prop="typeName" label="接入类型" width="110" /><el-table-column label="关联易积水点" min-width="160"><template #default="{ row }">{{ row.pointName || '未关联' }}</template></el-table-column>
@@ -20,7 +20,7 @@
     <pagination v-show="total > 0" v-model:page="query.pageNum" v-model:limit="query.pageSize" :total="total" @pagination="load" />
     <el-dialog v-model="bindingVisible" title="排水视频关联易积水点" width="520px" :close-on-click-modal="!saving" :show-close="!saving" :close-on-press-escape="!saving">
       <el-form label-width="100px"><el-form-item label="视频设备">{{ bindingRow?.deviceName }}</el-form-item>
-        <el-form-item label="易积水点" required><el-select v-model="pointId" filterable style="width:100%" placeholder="请选择基础数据中的易积水点" :disabled="saving"><el-option v-for="p in points" :key="p.id" :value="p.id" :label="`${p.name}(${p.area || '未设置区域'})`" /></el-select></el-form-item>
+        <el-form-item label="易积水点" required><el-select v-model="pointId" filterable style="width:100%" placeholder="请选择基础数据中的易积水点" :disabled="saving"><el-option v-for="p in points" :key="p.id" :value="p.id" :label="`${p.name}(${districtLabel(p.area) || '未设置片区'})`" /></el-select></el-form-item>
       </el-form>
       <el-alert title="一个点位可关联多个视频;每个视频只能关联一个点位。原有监测设备关联会保留。" type="info" :closable="false" />
       <template #footer><el-button :disabled="saving" @click="bindingVisible=false">取消</el-button><el-button type="primary" :loading="saving" :disabled="!pointId" @click="bind">保存</el-button></template>
@@ -29,11 +29,29 @@
   </div>
 </template>
 <script setup name="Spgngl">
-import { reactive, ref, onDeactivated } from 'vue'
+import { computed, reactive, ref, onDeactivated } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { listDrainageVideos, getDrainageVideoPoints, associateDrainageVideo } from '@/api/drainageVideo'
-import { districts } from '@/utils/drainageVideoModel'
+import { useDict } from '@/utils/dict'
 import DrainageLiveTile from './DrainageLiveTile.vue'
+
+/** 所属片区读字典 drainage_district(与 DistrictEnum 的 0城南/1城北/2太常 对齐) */
+const { drainage_district: districtDict } = useDict('drainage_district')
+const DISTRICT_FALLBACK = [
+  { value: '0', label: '城南' },
+  { value: '1', label: '城北' },
+  { value: '2', label: '太常' }
+]
+const districtOptions = computed(() =>
+  districtDict.value && districtDict.value.length ? districtDict.value : DISTRICT_FALLBACK
+)
+
+/** 易积水点入参里的 area 即片区编码,展示前统一转成中文;空值返回空串由调用处兜底 */
+function districtLabel(value) {
+  if (value === null || value === undefined || value === '') return ''
+  const item = districtOptions.value.find((d) => String(d.value) === String(value))
+  return item?.label || String(value)
+}
 const query = reactive({ pageNum:1, pageSize:10, query:'', district:undefined, unbound:undefined })
 const rows=ref([]), total=ref(0), loading=ref(false), error=ref(''), viewer=ref(null), points=ref([])
 const bindingVisible=ref(false), bindingRow=ref(null), pointId=ref(''), saving=ref(false)

+ 64 - 109
src/views/subSystem/pipeNetwork/assetInfo/equipmentBase/index.vue

@@ -3,10 +3,7 @@
   <div class="app-container equipment-page">
     <!-- 页头 -->
     <div class="page-head">
-      <div class="page-head-titles">
-        <div class="page-title">设备统计</div>
-      </div>
-      <el-button type="primary" icon="Plus" class="btn-primary" @click="handleAdd">新增设备</el-button>
+      <div class="page-title">设备统计</div>
     </div>
 
     <!-- KPI -->
@@ -46,34 +43,20 @@
 
     <!-- 查询条件 -->
     <el-card shadow="never" class="filter-card">
-      <div class="type-filter">
-        <div class="type-filter-title">设备类型</div>
-        <div class="type-filter-label">一级类型</div>
-        <div class="type-filter-options" role="group" aria-label="一级类型">
-          <button type="button" class="type-category" :aria-pressed="selectedCategoryId === undefined" @click="selectCategory(undefined)">全部</button>
+      <!-- 设备类型:本子系统下只有几个类型,直接一层平铺,不再分一级/二级 -->
+      <div v-if="equipmentSubtypes.length" class="type-filter">
+        <span class="filter-label">设备类型</span>
+        <div class="type-filter-options" role="group" aria-label="设备类型">
+          <button type="button" class="type-chip" :aria-pressed="queryParams.equipmentTypeId === undefined" @click="selectEquipmentType(undefined)">全部</button>
           <button
-            v-for="item in equipmentTypeTree"
+            v-for="item in equipmentSubtypes"
             :key="item.id"
             type="button"
-            class="type-category"
-            :aria-pressed="selectedCategoryId === item.id"
-            @click="selectCategory(item.id)"
+            class="type-chip"
+            :aria-pressed="queryParams.equipmentTypeId === item.id"
+            @click="selectEquipmentType(item.id)"
           >{{ item.typeName }}</button>
         </div>
-        <template v-if="selectedCategory">
-          <div class="type-filter-label">二级类型</div>
-          <div class="type-filter-options" role="group" aria-label="二级类型">
-            <button type="button" class="type-chip" :aria-pressed="queryParams.equipmentTypeId === undefined" @click="selectEquipmentType(undefined)">全部</button>
-            <button
-              v-for="item in equipmentSubtypes"
-              :key="item.id"
-              type="button"
-              class="type-chip"
-              :aria-pressed="queryParams.equipmentTypeId === item.id"
-              @click="selectEquipmentType(item.id)"
-            >{{ item.typeName }}</button>
-          </div>
-        </template>
       </div>
       <el-form ref="queryRef" :model="queryParams" :inline="true" class="filters">
         <el-form-item label="设备编码" prop="equipmentCode">
@@ -94,15 +77,13 @@
             @keyup.enter="handleQuery"
           />
         </el-form-item>
-        <el-form-item label="在线状态" prop="onlineStatus">
-          <el-select v-model="queryParams.onlineStatus" clearable placeholder="请选择" class="select--online">
-            <el-option label="在线" :value="1" />
-            <el-option label="离线" :value="0" />
+        <el-form-item label="运行状态">
+          <el-select v-model="runStatus" clearable placeholder="全部" class="select--status">
+            <el-option label="在线" value="online" />
+            <el-option label="离线" value="offline" />
+            <el-option label="报警" value="alarm" />
           </el-select>
         </el-form-item>
-        <el-form-item label="报警" prop="alarmOnly">
-          <el-checkbox v-model="alarmOnly" @change="handleQuery" />
-        </el-form-item>
         <el-form-item>
           <el-button type="primary" class="btn-query" @click="handleQuery">查询</el-button>
           <el-button class="btn-reset" @click="resetQuery">重置</el-button>
@@ -111,6 +92,11 @@
     </el-card>
 
     <el-card shadow="never" class="ledger">
+      <!-- 台账工具条:新增入口跟列表放在一起,不再飘在页面右上角 -->
+      <div class="ledger-toolbar">
+        <el-button type="primary" icon="Plus" class="btn-primary" @click="handleAdd">新增设备</el-button>
+        <span v-if="total" class="ledger-total">共 {{ total }} 台</span>
+      </div>
       <div v-loading="loading" class="grid">
         <el-empty v-if="!equipmentList.length" description="暂无设备数据" />
         <div
@@ -416,12 +402,13 @@ const rootTypeId = ref(route.query.rootTypeId ? Number(route.query.rootTypeId) :
 
 const equipmentList = ref([]);
 const allTypeList = ref([]);
-const selectedCategoryId = ref();
 const loading = ref(false);
 const open = ref(false);
 const detailOpen = ref(false);
 const mapOpen = ref(false);
-const alarmOnly = ref(false);
+// 运行状态筛选:'' 不限 / online 在线 / offline 离线 / alarm 报警
+// 后端把在线状态与报警状态分成两个参数,这里收在一个下拉里,避免并排放两个控件互相冲突
+const runStatus = ref("");
 const mapCenter = ref({ lng: 110.3965, lat: 28.4638 });
 const mapPoints = ref([]);
 const total = ref(0);
@@ -543,22 +530,20 @@ const equipmentTypeTree = computed(() => {
   return branch ? [branch] : tree;
 });
 
-const selectedCategory = computed(() => equipmentTypeTree.value.find((item) => item.id === selectedCategoryId.value));
-
-// 复用真实类型树;若存在更深层级,将可选叶子平铺在二级区域。
+// 复用真实类型树;若存在更深层级,将可选叶子平铺在筛选行。
 function collectSelectableTypes(nodes) {
   return nodes.flatMap((node) => node.children?.length
     ? collectSelectableTypes(node.children)
     : node.disabled ? [] : [node]);
 }
 
-const equipmentSubtypes = computed(() => collectSelectableTypes(selectedCategory.value?.children || []));
-
-function selectCategory(id) {
-  selectedCategoryId.value = id;
-  queryParams.value.equipmentTypeId = undefined;
-  handleQuery();
-}
+/**
+ * 可选的设备类型
+ *
+ * equipmentTypeTree 已按菜单注入的 rootTypeId 收窄到本子系统(排水只有 6 个叶子类型),
+ * 数量不多,直接一层平铺成筛选标签,不再做一级/二级两级联动。
+ */
+const equipmentSubtypes = computed(() => collectSelectableTypes(equipmentTypeTree.value));
 
 function selectEquipmentType(id) {
   queryParams.value.equipmentTypeId = id;
@@ -589,8 +574,7 @@ const data = reactive({
     pageSize: 10,
     equipmentCode: undefined,
     equipmentName: undefined,
-    equipmentTypeId: undefined,
-    onlineStatus: undefined
+    equipmentTypeId: undefined
   },
   form: {},
   rules: {
@@ -630,13 +614,13 @@ function createEmptyForm() {
 
 function getList() {
   loading.value = true;
+  const status = runStatus.value;
   return listEquipment({
     ...queryParams.value,
     rootTypeId: rootTypeId.value,
-    // 保留菜单的数据范围,大类筛选通过子类型集合交由后端分页过滤。
-    // 空大类使用无效 ID 返回空结果,避免空参数被后端解释为不限类型。
-    equipmentTypeIds: selectedCategory.value ? equipmentSubtypes.value.map((item) => item.id).join(",") || "-1" : undefined,
-    alarmStatus: alarmOnly.value ? 1 : undefined
+    // 在线 / 离线走 onlineStatus,报警走 alarmStatus,两者互斥,不会同时传
+    onlineStatus: status === "online" ? 1 : status === "offline" ? 0 : undefined,
+    alarmStatus: status === "alarm" ? 1 : undefined
   })
     .then((response) => {
       equipmentList.value = response.records || [];
@@ -741,9 +725,8 @@ function handleQuery() {
 
 function resetQuery() {
   proxy.resetForm("queryRef");
-  selectedCategoryId.value = undefined;
   queryParams.value.equipmentTypeId = undefined;
-  alarmOnly.value = false;
+  runStatus.value = "";
   handleQuery();
 }
 
@@ -942,6 +925,18 @@ onMounted(() => {
   border-radius: 12px;
 }
 
+.ledger-toolbar {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.ledger-total {
+  margin-left: auto;
+  font-size: 13px;
+  color: #86909c;
+}
+
 .ledger :deep(.el-card__body) {
   display: flex;
   flex-direction: column;
@@ -951,38 +946,20 @@ onMounted(() => {
 
 /* 筛选栏 */
 .type-filter {
-  margin-bottom: 20px;
-}
-
-.type-filter-title,
-.type-filter-label {
   display: flex;
-  align-items: center;
-  gap: 8px;
-}
-
-.type-filter-title {
-  color: #4e5969;
-  font-size: 13px;
-}
-
-.type-filter-title::before,
-.type-filter-label::before {
-  content: "";
-  width: 3px;
-  height: 14px;
-  border-radius: 2px;
-  background: var(--el-color-primary);
+  align-items: flex-start;
+  gap: 12px;
+  margin-bottom: 16px;
+  padding-bottom: 16px;
+  border-bottom: 1px solid #f0f1f3;
 }
 
-.type-filter-label {
-  margin: 16px 0 8px;
-  color: #86909c;
+.filter-label {
+  flex: none;
+  padding-top: 6px;
+  color: #6b7280;
   font-size: 12px;
-}
-
-.type-filter-label::before {
-  background: #86909c;
+  line-height: 18px;
 }
 
 .type-filter-options {
@@ -991,12 +968,11 @@ onMounted(() => {
   gap: 8px;
 }
 
-.type-category,
 .type-chip {
-  padding: 0 16px;
-  height: 34px;
+  padding: 0 14px;
+  height: 30px;
   border: 1px solid #e5e6eb;
-  border-radius: 8px;
+  border-radius: 999px;
   background: #fff;
   color: #4e5969;
   font: inherit;
@@ -1006,39 +982,18 @@ onMounted(() => {
   transition: background .15s, color .15s, border-color .15s;
 }
 
-.type-category:hover,
 .type-chip:hover {
   color: var(--el-color-primary);
   border-color: var(--el-color-primary-light-5);
 }
 
-.type-category[aria-pressed="true"] {
-  color: #fff;
-  border-color: var(--el-color-primary);
-  background: var(--el-color-primary);
-  font-weight: 500;
-}
-
-.type-chip {
-  height: 30px;
-  padding: 0 14px;
-  border-color: transparent;
-  border-radius: 999px;
-  background: transparent;
-}
-
-.type-chip:hover,
-.type-chip[aria-pressed="true"] {
-  background: var(--el-color-primary-light-9);
-}
-
 .type-chip[aria-pressed="true"] {
   color: var(--el-color-primary);
   border-color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
   font-weight: 500;
 }
 
-.type-category:focus-visible,
 .type-chip:focus-visible {
   outline: 2px solid var(--el-color-primary);
   outline-offset: 2px;
@@ -1068,7 +1023,7 @@ onMounted(() => {
   width: 150px;
 }
 
-.select--online {
+.select--status {
   width: 120px;
 }
 

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio