Forráskód Böngészése

feat: 窨井盖地图坐标修复与燃气业务页真实数据化

- 窨井盖首页、设备一张图、实时监测、故障管理、状态监测报警统一做 WGS84→BD-09 转换,
  消除地图点位约 1.1 公里偏移;首页视野改为按设备范围自适应,单点或两点过近时定位到 zoom 16
- 窨井盖统计分析页重写:新增 KPI 指标条,图表固定高度,空数据用覆盖层提示并保留区域坐标轴
- 燃气管网数据页只读化,操作列仅保留详情抽屉,导出支持勾选
- 新增燃气管点台账页,补齐燃气首个可维护的列表页
- 维修记录改用真实管段数据,按累计维修次数分四档健康等级,碎段用光晕加中点标签标注,
  负责人、维修单位、维修类型改取系统人员、部门与字典
- 燃气监测评估、隐患与资产类页面统一若依版式:按钮改 plain、表格去 stripe/border、
  分页换全局 pagination 组件
null 1 hete
szülő
commit
1b0c688f20
35 módosított fájl, 3059 hozzáadás és 2140 törlés
  1. 179 331
      src/views/subSystem/basic/EquipmentAbnormal.vue
  2. 121 114
      src/views/subSystem/basic/GasPipePoint.vue
  3. 235 163
      src/views/subSystem/basic/Manhole.vue
  4. 149 191
      src/views/subSystem/basic/PipeNetwork.vue
  5. 599 256
      src/views/subSystem/basic/RepairRecord.vue
  6. 544 0
      src/views/subSystem/gas/gasBasicadata/gasPipePoint.vue
  7. 1 1
      src/views/subSystem/gas/gasBasicadata/gasPipelintNet.vue
  8. 1 1
      src/views/subSystem/gas/gasBasicadata/gasTestingeq.vue
  9. 1 1
      src/views/subSystem/gas/gasOperamon/czspjk.vue
  10. 16 9
      src/views/subSystem/gas/gasOperamon/gxlljc.vue
  11. 17 9
      src/views/subSystem/gas/gasOperamon/gxyljc.vue
  12. 1 1
      src/views/subSystem/gas/gasOperamon/ssjcyzt.vue
  13. 2 2
      src/views/subSystem/gas/gasOperamon/xldxkjkrqtndjc.vue
  14. 1 1
      src/views/subSystem/gas/gasfxpg/gwfxpg.vue
  15. 2 2
      src/views/subSystem/gas/gasfxpg/gwfxpgqd.vue
  16. 2 2
      src/views/subSystem/gas/gasfxpg/gwfxxq.vue
  17. 1 1
      src/views/subSystem/gas/gasfxpg/gxfxpgsst.vue
  18. 2 2
      src/views/subSystem/gas/gashiddendanger/gasApicture.vue
  19. 17 10
      src/views/subSystem/gas/gasjcyj/gxllyctx.vue
  20. 17 15
      src/views/subSystem/gas/gasjcyj/gxylyctx.vue
  21. 17 10
      src/views/subSystem/gas/gasjcyj/krqindbj.vue
  22. 1 1
      src/views/subSystem/gas/gasjcyj/rqjcbjyzt.vue
  23. 2 2
      src/views/subSystem/gas/gasjcyj/xldxkjkrqindbj.vue
  24. 210 132
      src/views/subSystem/gas/ghome/gHome.vue
  25. 108 100
      src/views/subSystem/hidden/HazardList.vue
  26. 143 161
      src/views/subSystem/hidden/HazardOperationPage.vue
  27. 1 14
      src/views/subSystem/hidden/yhpc.vue
  28. 22 6
      src/views/subSystem/manholeCover/alarm/alarmSanalysis.vue
  29. 12 4
      src/views/subSystem/manholeCover/device/deviceApicture.vue
  30. 230 46
      src/views/subSystem/manholeCover/device/statistics.vue
  31. 190 43
      src/views/subSystem/manholeCover/home/index.vue
  32. 11 3
      src/views/subSystem/manholeCover/mRealTimemonit/mRStateMonitoring.vue
  33. 11 3
      src/views/subSystem/manholeCover/operation/faultMana.vue
  34. 111 421
      src/views/subSystem/pipeNetwork/assetInfo/equipmentBase/index.vue
  35. 82 82
      src/views/subSystem/pipeNetwork/assetInfo/monitorPoint/index.vue

+ 179 - 331
src/views/subSystem/basic/EquipmentAbnormal.vue

@@ -1,17 +1,19 @@
 <template>
-  <div class="equipment-abnormal">
-    <!-- 左侧面板 -->
-    <div class="left-panel">
-      <!-- 搜索 -->
-      <div class="panel-card search-card">
-        <div class="panel-title"><el-icon><Search /></el-icon> 快速查询</div>
-        <el-input v-model="searchKey" placeholder="设备名称/编号/地址" clearable prefix-icon="Search" size="default" />
-        <el-select v-model="levelFilter" placeholder="异常等级" clearable style="width:100%;margin-top:10px">
+  <div class="app-container">
+    <!-- 查询条件 -->
+    <el-form ref="queryRef" :inline="true" :model="queryParams" v-show="showSearch" label-width="80px">
+      <el-form-item label="关键字" prop="searchKey">
+        <el-input v-model="queryParams.searchKey" placeholder="设备名称/编号/地址" clearable style="width: 220px" @keyup.enter="handleQuery" />
+      </el-form-item>
+      <el-form-item label="异常等级" prop="level">
+        <el-select v-model="queryParams.level" placeholder="全部" clearable style="width: 160px">
           <el-option label="严重" value="严重" />
           <el-option label="一般" value="一般" />
           <el-option label="轻微" value="轻微" />
         </el-select>
-        <el-select v-model="typeFilter" placeholder="设备类型" clearable style="width:100%;margin-top:10px">
+      </el-form-item>
+      <el-form-item label="设备类型" prop="type">
+        <el-select v-model="queryParams.type" placeholder="全部" clearable style="width: 160px">
           <el-option label="阀门" value="阀门" />
           <el-option label="调压器" value="调压器" />
           <el-option label="流量计" value="流量计" />
@@ -19,102 +21,73 @@
           <el-option label="终端节点" value="终端节点" />
           <el-option label="检查井" value="检查井" />
         </el-select>
-        <el-select v-model="statusFilter" placeholder="处理状态" clearable style="width:100%;margin-top:10px">
+      </el-form-item>
+      <el-form-item label="处理状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 160px">
           <el-option label="待处理" value="待处理" />
           <el-option label="处理中" value="处理中" />
           <el-option label="已修复" value="已修复" />
         </el-select>
-        <div style="display:flex;gap:8px;margin-top:10px">
-          <el-button type="primary" size="small" @click="handleQuery" style="flex:1">查询</el-button>
-          <el-button size="small" @click="handleReset" style="flex:1">重置</el-button>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+        <el-button icon="Refresh" @click="handleReset">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="loadList" />
+    </el-row>
+
+    <!-- 统计卡片 -->
+    <div class="stats-row">
+      <div class="stat-card stat-total">
+        <div class="stat-icon"><el-icon size="22"><WarningFilled /></el-icon></div>
+        <div class="stat-info">
+          <div class="stat-label">异常设备总数</div>
+          <div class="stat-value">{{ stats.total }} <small>台</small></div>
         </div>
       </div>
-
-      <!-- 异常等级分布 -->
-      <div class="panel-card">
-        <div class="panel-title"><el-icon><PieChart /></el-icon> 异常等级分布</div>
-        <div class="level-list">
-          <div class="level-item critical">
-            <span class="level-dot"></span>
-            <span class="level-label">严重</span>
-            <span class="level-count">{{ stats.critical }}</span>
-          </div>
-          <div class="level-item warning">
-            <span class="level-dot"></span>
-            <span class="level-label">一般</span>
-            <span class="level-count">{{ stats.normal }}</span>
-          </div>
-          <div class="level-item minor">
-            <span class="level-dot"></span>
-            <span class="level-label">轻微</span>
-            <span class="level-count">{{ stats.minor }}</span>
-          </div>
+      <div class="stat-card stat-today">
+        <div class="stat-icon"><el-icon size="22"><Clock /></el-icon></div>
+        <div class="stat-info">
+          <div class="stat-label">今日新增</div>
+          <div class="stat-value">{{ stats.today }} <small>台</small></div>
         </div>
       </div>
-
-      <!-- 设备类型统计 -->
-      <div class="panel-card">
-        <div class="panel-title"><el-icon><Histogram /></el-icon> 设备类型统计</div>
-        <div class="type-list">
-          <div class="type-item" v-for="item in deviceTypeStats" :key="item.name">
-            <span class="type-name">{{ item.name }}</span>
-            <span class="type-bar-bg"><span class="type-bar" :style="{width: (stats.total > 0 ? item.count/stats.total*100 : 0)+'%', background: item.color}"></span></span>
-            <span class="type-count">{{ item.count }}</span>
-          </div>
+      <div class="stat-card stat-rate">
+        <div class="stat-icon"><el-icon size="22"><TrendCharts /></el-icon></div>
+        <div class="stat-info">
+          <div class="stat-label">异常占比</div>
+          <div class="stat-value">{{ stats.rate }} <small>%</small></div>
         </div>
       </div>
-    </div>
-
-    <!-- 右侧内容区 -->
-    <div class="right-panel">
-      <!-- 统计卡片 -->
-      <div class="stats-row">
-        <div class="stat-card stat-total">
-          <div class="stat-icon"><el-icon size="22"><WarningFilled /></el-icon></div>
-          <div class="stat-info">
-            <div class="stat-label">异常设备总数</div>
-            <div class="stat-value">{{ stats.total }} <small>台</small></div>
-          </div>
-        </div>
-        <div class="stat-card stat-today">
-          <div class="stat-icon"><el-icon size="22"><Clock /></el-icon></div>
-          <div class="stat-info">
-            <div class="stat-label">今日新增</div>
-            <div class="stat-value">{{ stats.today }} <small>台</small></div>
-          </div>
-        </div>
-        <div class="stat-card stat-rate">
-          <div class="stat-icon"><el-icon size="22"><TrendCharts /></el-icon></div>
-          <div class="stat-info">
-            <div class="stat-label">异常占比</div>
-            <div class="stat-value">{{ stats.rate }} <small>%</small></div>
-          </div>
-        </div>
-        <div class="stat-card stat-processed">
-          <div class="stat-icon"><el-icon size="22"><CircleCheckFilled /></el-icon></div>
-          <div class="stat-info">
-            <div class="stat-label">已修复</div>
-            <div class="stat-value">{{ stats.processed }} <small>台</small></div>
-          </div>
+      <div class="stat-card stat-processed">
+        <div class="stat-icon"><el-icon size="22"><CircleCheckFilled /></el-icon></div>
+        <div class="stat-info">
+          <div class="stat-label">已修复</div>
+          <div class="stat-value">{{ stats.processed }} <small>台</small></div>
         </div>
-        <div class="stat-card stat-pending">
-          <div class="stat-icon"><el-icon size="22"><Warning /></el-icon></div>
-          <div class="stat-info">
-            <div class="stat-label">待处理</div>
-            <div class="stat-value">{{ stats.pending }} <small>台</small></div>
-          </div>
+      </div>
+      <div class="stat-card stat-pending">
+        <div class="stat-icon"><el-icon size="22"><Warning /></el-icon></div>
+        <div class="stat-info">
+          <div class="stat-label">待处理</div>
+          <div class="stat-value">{{ stats.pending }} <small>台</small></div>
         </div>
-        <div class="stat-card stat-process-rate">
-          <div class="stat-icon"><el-icon size="22"><DataAnalysis /></el-icon></div>
-          <div class="stat-info">
-            <div class="stat-label">修复率</div>
-            <div class="stat-value">{{ stats.processRate }} <small>%</small></div>
-          </div>
+      </div>
+      <div class="stat-card stat-process-rate">
+        <div class="stat-icon"><el-icon size="22"><DataAnalysis /></el-icon></div>
+        <div class="stat-info">
+          <div class="stat-label">修复率</div>
+          <div class="stat-value">{{ stats.processRate }} <small>%</small></div>
         </div>
       </div>
+    </div>
 
-      <!-- 图表区域 - 上方: 趋势图(全宽) -->
-      <div class="chart-card chart-trend">
+    <!-- 趋势图 -->
+    <el-card shadow="never" class="chart-card">
+      <template #header>
         <div class="chart-header">
           <span class="chart-title">设备异常趋势图</span>
           <span class="chart-subtitle">近15天运行数据</span>
@@ -124,88 +97,93 @@
             <span class="legend-tag tag-fixed">已修复</span>
           </div>
         </div>
-        <div id="trendChart" class="chart-body chart-body-large" style="height:340px"></div>
-      </div>
-
-      <!-- 图表区域 - 下方: 两个图表并排 -->
-      <div class="charts-row">
-        <div class="chart-card chart-half">
-          <div class="chart-header">
-            <span class="chart-title">异常类型分布</span>
-          </div>
-          <div id="typeChart" class="chart-body" style="height:290px"></div>
-        </div>
-        <div class="chart-card chart-half">
-          <div class="chart-header">
-            <span class="chart-title">设备异常等级与处理状态</span>
-          </div>
-          <div id="levelChart" class="chart-body" style="height:290px"></div>
-        </div>
-      </div>
+      </template>
+      <div id="trendChart" style="height: 340px"></div>
+    </el-card>
+
+    <!-- 双图并排 -->
+    <div class="charts-row">
+      <el-card shadow="never" class="chart-card">
+        <template #header>
+          <div class="chart-header"><span class="chart-title">异常类型分布</span></div>
+        </template>
+        <div id="typeChart" style="height: 290px"></div>
+      </el-card>
+      <el-card shadow="never" class="chart-card">
+        <template #header>
+          <div class="chart-header"><span class="chart-title">设备异常等级与处理状态</span></div>
+        </template>
+        <div id="levelChart" style="height: 290px"></div>
+      </el-card>
+    </div>
 
-      <!-- 异常设备列表 -->
-      <div class="table-card">
-        <div class="table-header">
-          <span class="table-title">异常设备列表</span>
-          <span class="table-count">共 {{ tableTotal }} 条记录</span>
-        </div>
-        <el-table :data="paginatedData" border stripe v-loading="loading" max-height="460" size="small"
-          @sort-change="handleSortChange" :default-sort="{ prop: 'alarmTime', order: 'descending' }">
-          <el-table-column prop="id" label="序号" width="56" align="center" sortable="custom" />
-          <el-table-column prop="deviceCode" label="设备编号" min-width="140" show-overflow-tooltip />
-          <el-table-column prop="deviceName" label="设备名称" min-width="150" show-overflow-tooltip />
-          <el-table-column prop="deviceType" label="设备类型" width="100" />
-          <el-table-column prop="level" label="异常等级" width="100" align="center" sortable="custom">
-            <template #default="{ row }">
-              <el-tag :type="levelTagType(row.level)" size="small">{{ row.level }}</el-tag>
-            </template>
-          </el-table-column>
-          <el-table-column prop="alarmDesc" label="异常描述" min-width="220" show-overflow-tooltip />
-          <el-table-column prop="address" label="安装地址" min-width="200" show-overflow-tooltip />
-          <el-table-column prop="alarmTime" label="异常时间" width="168" sortable="custom" />
-          <el-table-column prop="processStatus" label="处理状态" width="100" align="center" sortable="custom">
-            <template #default="{ row }">
-              <el-tag :type="row.processStatus === '已修复' ? 'success' : row.processStatus === '处理中' ? 'warning' : 'danger'" size="small">
-                {{ row.processStatus }}
-              </el-tag>
-            </template>
-          </el-table-column>
-          <el-table-column label="操作" width="150" fixed="right" align="center">
-            <template #default="{ row }">
-              <el-button type="primary" link size="small" @click="handleDetail(row)">详情</el-button>
-              <el-button v-if="row.processStatus === '待处理'" type="warning" link size="small" @click="handleProcess(row)">处理</el-button>
-              <el-button v-if="row.processStatus === '处理中'" type="success" link size="small" @click="handleComplete(row)">完成</el-button>
-            </template>
-          </el-table-column>
-        </el-table>
-        <div class="table-footer">
-          <el-pagination
-            v-model:current-page="currentPage"
-            v-model:page-size="pageSize"
-            :total="tableTotal"
-            layout="total, prev, pager, next"
-            :page-sizes="[10, 20]"
-            small
-            background
-          />
+    <!-- 异常设备列表 -->
+    <el-card shadow="never" class="chart-card">
+      <template #header>
+        <div class="chart-header">
+          <span class="chart-title">异常设备列表</span>
+          <span class="chart-subtitle">共 {{ tableTotal }} 条记录</span>
         </div>
-      </div>
-    </div>
+      </template>
+      <el-table
+        :data="paginatedData"
+        v-loading="loading"
+        @sort-change="handleSortChange"
+        :default-sort="{ prop: 'alarmTime', order: 'descending' }"
+      >
+        <el-table-column prop="id" label="序号" width="70" align="center" sortable="custom" />
+        <el-table-column prop="deviceCode" label="设备编号" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="deviceName" label="设备名称" min-width="150" show-overflow-tooltip />
+        <el-table-column prop="deviceType" label="设备类型" width="110" />
+        <el-table-column prop="level" label="异常等级" width="100" align="center" sortable="custom">
+          <template #default="{ row }">
+            <el-tag :type="levelTagType(row.level)" size="small">{{ row.level }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="alarmDesc" label="异常描述" min-width="220" show-overflow-tooltip />
+        <el-table-column prop="address" label="安装地址" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="alarmTime" label="异常时间" width="168" sortable="custom" />
+        <el-table-column prop="processStatus" label="处理状态" width="100" align="center" sortable="custom">
+          <template #default="{ row }">
+            <el-tag :type="row.processStatus === '已修复' ? 'success' : row.processStatus === '处理中' ? 'warning' : 'danger'" size="small">
+              {{ row.processStatus }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="160" fixed="right" align="center" class-name="small-padding fixed-width">
+          <template #default="{ row }">
+            <el-button link type="primary" icon="View" @click="handleDetail(row)">详情</el-button>
+            <el-button v-if="row.processStatus === '待处理'" link type="primary" icon="Edit" @click="handleProcess(row)">处理</el-button>
+            <el-button v-if="row.processStatus === '处理中'" link type="success" icon="Select" @click="handleComplete(row)">完成</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-show="tableTotal > 0"
+        :total="tableTotal"
+        v-model:page="currentPage"
+        v-model:limit="pageSize"
+        :page-sizes="[10, 20]"
+        @pagination="loadList"
+      />
+    </el-card>
   </div>
 </template>
 
 <script setup>
-import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
+import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
 import * as echarts from 'echarts'
-import { Clock, CircleCheckFilled, DataAnalysis, Histogram, PieChart, Search, TrendCharts, Warning, WarningFilled } from '@element-plus/icons-vue'
+import { Clock, CircleCheckFilled, DataAnalysis, TrendCharts, Warning, WarningFilled } from '@element-plus/icons-vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { getEquipmentAbnormalList, getEquipmentAbnormalStats } from '@/api/pipeNetwork/gasMonitor'
 
+defineOptions({ name: 'EquipmentAbnormal' })
+
 const loading = ref(false)
-const searchKey = ref('')
-const levelFilter = ref('')
-const typeFilter = ref('')
-const statusFilter = ref('')
+const showSearch = ref(true)
+const queryRef = ref(null)
+const queryParams = reactive({ searchKey: '', level: '', type: '', status: '' })
 const currentPage = ref(1)
 const pageSize = ref(10)
 const sortProp = ref('alarmTime')
@@ -221,8 +199,6 @@ const stats = ref({
   processRate: 0, critical: 0, normal: 0, minor: 0
 })
 
-const deviceTypeStats = ref([])
-
 async function loadStats() {
   try {
     const res = await getEquipmentAbnormalStats()
@@ -234,13 +210,6 @@ async function loadStats() {
         processRate: d.processRate || 0, critical: d.critical || 0,
         normal: d.normal || 0, minor: d.minor || 0
       }
-      // 设备类型统计
-      const colors = ['#e63946', '#f77f00', '#2d6a4f', '#7f4f24', '#6c757d', '#b08968']
-      const typeMap = d.deviceTypeStats || {}
-      let idx = 0
-      deviceTypeStats.value = Object.entries(typeMap).map(([name, count]) => ({
-        name, count, color: colors[idx++ % colors.length]
-      }))
       // 图表数据
       chartData.value = {
         trend: d.trend || { dates: [], totalData: [], newData: [], fixedData: [] },
@@ -268,10 +237,10 @@ async function loadList() {
     const res = await getEquipmentAbnormalList({
       pageNum: currentPage.value,
       pageSize: pageSize.value,
-      searchKey: searchKey.value || undefined,
-      level: levelFilter.value || undefined,
-      type: typeFilter.value || undefined,
-      status: statusFilter.value || undefined
+      searchKey: queryParams.searchKey || undefined,
+      level: queryParams.level || undefined,
+      type: queryParams.type || undefined,
+      status: queryParams.status || undefined
     })
     if (res.data) {
       tableData.value = (res.data.rows || []).map((r, i) => ({
@@ -310,10 +279,6 @@ function handleSortChange({ prop, order }) {
   sortOrder.value = order || 'descending'
 }
 
-// 分页切换时重载
-watch(currentPage, () => loadList())
-watch(pageSize, () => { currentPage.value = 1; loadList() })
-
 // ==================== 工具函数 ====================
 function levelTagType(level) {
   if (level === '严重') return 'danger'
@@ -327,10 +292,8 @@ function handleQuery() {
 }
 
 function handleReset() {
-  searchKey.value = ''
-  levelFilter.value = ''
-  typeFilter.value = ''
-  statusFilter.value = ''
+  queryRef.value?.resetFields()
+  Object.assign(queryParams, { searchKey: '', level: '', type: '', status: '' })
   currentPage.value = 1
   loadList()
 }
@@ -341,7 +304,7 @@ function handleDetail(row) {
       <p><b>设备编号:</b>${row.deviceCode}</p>
       <p><b>设备名称:</b>${row.deviceName}</p>
       <p><b>设备类型:</b>${row.deviceType}</p>
-      <p><b>异常等级:</b><span style="color:${row.level==='严重'?'#e63946':row.level==='一般'?'#f77f00':'#909399'}">${row.level}</span></p>
+      <p><b>异常等级:</b><span style="color:${row.level === '严重' ? '#e63946' : row.level === '一般' ? '#f77f00' : '#909399'}">${row.level}</span></p>
       <p><b>异常描述:</b>${row.alarmDesc}</p>
       <p><b>安装地址:</b>${row.address}</p>
       <p><b>异常时间:</b>${row.alarmTime}</p>
@@ -543,112 +506,34 @@ onBeforeUnmount(() => {
 </script>
 
 <style scoped>
-.equipment-abnormal {
-  display: flex;
-  gap: 16px;
-  padding: 16px;
-  background: #f0f2f6;
-  min-height: calc(100vh - 84px);
-}
-
-/* ========== 左侧面板 ========== */
-.left-panel {
-  width: 280px;
-  flex-shrink: 0;
-  display: flex;
-  flex-direction: column;
-  gap: 14px;
-}
-
-.panel-card {
-  background: #fff;
-  border-radius: 12px;
-  padding: 16px;
-  box-shadow: 0 2px 12px rgba(15, 35, 95, 0.04);
-}
-
-.panel-title {
-  font-size: 14px;
-  font-weight: 700;
-  color: #1f2d3d;
-  margin-bottom: 14px;
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  padding-bottom: 10px;
-  border-bottom: 1px solid #f0f2f6;
-}
-
-.panel-title .el-icon { color: #1890ff; }
-
-/* 等级列表 */
-.level-list { display: flex; flex-direction: column; gap: 8px; }
-.level-item {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  padding: 8px 12px;
-  border-radius: 8px;
-  background: #f8fafc;
-  cursor: pointer;
-  transition: all 0.2s;
-}
-.level-item:hover { background: #eef2f8; transform: translateX(4px); }
-.level-dot { width: 10px; height: 10px; border-radius: 50%; }
-.level-item.critical .level-dot { background: #e63946; box-shadow: 0 0 6px rgba(230,57,70,0.4); }
-.level-item.warning .level-dot { background: #f77f00; box-shadow: 0 0 6px rgba(247,127,0,0.4); }
-.level-item.minor .level-dot { background: #457b9d; box-shadow: 0 0 6px rgba(69,123,157,0.4); }
-.level-label { flex: 1; font-size: 13px; color: #4e5969; }
-.level-count { font-size: 18px; font-weight: 700; color: #1f2d3d; }
-
-/* 设备类型列表 */
-.type-list { display: flex; flex-direction: column; gap: 10px; }
-.type-item { display: flex; align-items: center; gap: 10px; }
-.type-name { width: 56px; font-size: 12px; color: #606a78; text-align: right; flex-shrink: 0; }
-.type-bar-bg { flex: 1; height: 8px; background: #f0f2f6; border-radius: 4px; overflow: hidden; }
-.type-bar { height: 100%; border-radius: 4px; transition: width 0.6s ease; }
-.type-count { width: 24px; font-size: 12px; font-weight: 600; color: #1f2d3d; text-align: right; flex-shrink: 0; }
-
-/* ========== 右侧内容区 ========== */
-.right-panel {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  gap: 16px;
-  min-width: 0;
-}
-
 /* 统计卡片 */
 .stats-row {
   display: grid;
   grid-template-columns: repeat(6, 1fr);
   gap: 12px;
+  margin-bottom: 16px;
 }
 
 .stat-card {
-  background: #fff;
-  border-radius: 12px;
-  padding: 14px 16px;
-  box-shadow: 0 2px 12px rgba(15, 35, 95, 0.04);
   display: flex;
   align-items: center;
   gap: 14px;
-  cursor: pointer;
-  transition: all 0.25s;
-}
-.stat-card:hover {
-  transform: translateY(-2px);
-  box-shadow: 0 6px 20px rgba(15, 35, 95, 0.1);
+  padding: 14px 16px;
+  background: #fff;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
 }
 
 .stat-icon {
-  width: 44px; height: 44px;
-  border-radius: 10px;
   display: flex;
   align-items: center;
   justify-content: center;
+  width: 44px;
+  height: 44px;
   flex-shrink: 0;
+  border-radius: 10px;
 }
+
 .stat-total .stat-icon { background: #fef0f0; color: #e63946; }
 .stat-today .stat-icon { background: #fff3e6; color: #f77f00; }
 .stat-rate .stat-icon { background: #fef5e7; color: #e76f51; }
@@ -656,34 +541,40 @@ onBeforeUnmount(() => {
 .stat-pending .stat-icon { background: #eaf2f8; color: #457b9d; }
 .stat-process-rate .stat-icon { background: #e8f5e9; color: #2d6a4f; }
 
-.stat-label { font-size: 12px; color: #9098a6; margin-bottom: 4px; }
-.stat-value { font-size: 24px; font-weight: 700; color: #1f2d3d; line-height: 1.1; }
-.stat-value small { font-size: 12px; color: #9098a6; font-weight: 400; }
+.stat-label {
+  margin-bottom: 4px;
+  font-size: 12px;
+  color: #9098a6;
+}
 
-/* 图表卡片 */
-.chart-card {
-  background: #fff;
-  border-radius: 12px;
-  padding: 16px 20px;
-  box-shadow: 0 2px 12px rgba(15, 35, 95, 0.04);
-  display: flex;
-  flex-direction: column;
+.stat-value {
+  font-size: 24px;
+  font-weight: 700;
+  line-height: 1.1;
+  color: #1f2d3d;
 }
 
-.chart-trend { /* 全宽 */ }
+.stat-value small {
+  font-size: 12px;
+  font-weight: 400;
+  color: #9098a6;
+}
+
+/* 图表 / 列表卡片 */
+.chart-card {
+  margin-bottom: 16px;
+}
 
 .charts-row {
   display: grid;
   grid-template-columns: 1fr 1fr;
-  gap: 14px;
+  gap: 16px;
 }
-.chart-half { flex: 1; }
 
 .chart-header {
   display: flex;
   align-items: center;
   gap: 12px;
-  margin-bottom: 6px;
   flex-wrap: wrap;
 }
 
@@ -705,65 +596,22 @@ onBeforeUnmount(() => {
 }
 
 .legend-tag {
-  font-size: 11px;
   padding: 2px 10px;
   border-radius: 10px;
+  font-size: 11px;
   color: #fff;
 }
+
 .tag-total { background: #e63946; }
 .tag-new { background: #f77f00; }
 .tag-fixed { background: #2a9d8f; }
 
-.chart-body {
-  flex: 1;
-  min-height: 280px;
-}
-.chart-body-large {
-  min-height: 330px;
-}
-
-/* 表格卡片 */
-.table-card {
-  background: #fff;
-  border-radius: 12px;
-  padding: 16px 20px;
-  box-shadow: 0 2px 12px rgba(15, 35, 95, 0.04);
-}
-
-.table-header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 12px;
-}
-
-.table-title {
-  font-size: 15px;
-  font-weight: 700;
-  color: #1f2d3d;
-}
-
-.table-count {
-  font-size: 12px;
-  color: #9098a6;
-}
-
-.table-footer {
-  display: flex;
-  justify-content: flex-end;
-  margin-top: 12px;
-}
-
-/* ========== 响应式 ========== */
+/* 响应式 */
 @media (max-width: 1600px) {
   .stats-row { grid-template-columns: repeat(3, 1fr); }
-  .left-panel { width: 260px; }
 }
 
 @media (max-width: 1400px) {
-  .equipment-abnormal { flex-direction: column; }
-  .left-panel { width: 100%; flex-direction: row; flex-wrap: wrap; }
-  .left-panel .panel-card { flex: 1; min-width: 240px; }
   .charts-row { grid-template-columns: 1fr; }
 }
 

+ 121 - 114
src/views/subSystem/basic/GasPipePoint.vue

@@ -1,80 +1,82 @@
 <template>
-  <div class="pipe-point-container">
-    <el-card class="search-card">
-      <el-form :inline="true" :model="searchForm" class="search-form">
-        <el-form-item label="管点名称">
-          <el-input v-model="searchForm.pointName" placeholder="请输入管点名称" clearable style="width:180px" />
-        </el-form-item>
-        <el-form-item label="管点编码">
-          <el-input v-model="searchForm.pointCode" placeholder="请输入管点编码" clearable style="width:180px" />
-        </el-form-item>
-        <el-form-item label="所属管线">
-          <el-select v-model="searchForm.networkId" placeholder="请选择" clearable style="width:200px">
-            <el-option v-for="opt in networkOptions" :key="opt.networkId" :label="opt.networkName" :value="opt.networkId" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="管点类型">
-          <el-select v-model="searchForm.pointType" placeholder="请选择" clearable style="width:160px">
-            <el-option v-for="d in pointTypeDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="状态">
-          <el-select v-model="searchForm.status" placeholder="请选择" clearable style="width:140px">
-            <el-option v-for="d in pointStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" @click="handleSearch">查询</el-button>
-          <el-button @click="handleReset">重置</el-button>
-          <el-button type="success" @click="handleAdd">新增管点</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
+  <div class="app-container">
+    <el-form ref="queryRef" :inline="true" :model="searchForm" v-show="showSearch" label-width="80px">
+      <el-form-item label="管点名称" prop="pointName">
+        <el-input v-model="searchForm.pointName" placeholder="请输入管点名称" clearable style="width: 200px" @keyup.enter="handleSearch" />
+      </el-form-item>
+      <el-form-item label="管点编码" prop="pointCode">
+        <el-input v-model="searchForm.pointCode" placeholder="请输入管点编码" clearable style="width: 200px" @keyup.enter="handleSearch" />
+      </el-form-item>
+      <el-form-item label="所属管线" prop="networkId">
+        <el-select v-model="searchForm.networkId" placeholder="请选择所属管线" clearable style="width: 220px">
+          <el-option v-for="opt in networkOptions" :key="opt.networkId" :label="opt.networkName" :value="opt.networkId" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="管点类型" prop="pointType">
+        <el-select v-model="searchForm.pointType" placeholder="请选择管点类型" clearable style="width: 180px">
+          <el-option v-for="d in pointTypeDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 160px">
+          <el-option v-for="d in pointStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
+        <el-button icon="Refresh" @click="handleReset">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="Plus" @click="handleAdd">新增管点</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="loadData" />
+    </el-row>
+
+    <el-table :data="tableData" v-loading="loading">
+      <el-table-column prop="pointCode" label="管点编码" min-width="130" show-overflow-tooltip />
+      <el-table-column prop="pointName" label="管点名称" min-width="150" show-overflow-tooltip />
+      <el-table-column prop="networkName" label="所属管线" min-width="150" show-overflow-tooltip />
+      <el-table-column prop="pointType" label="管点类型" width="110" align="center">
+        <template #default="{ row }">{{ getDictLabel(pointTypeDict, row.pointType) }}</template>
+      </el-table-column>
+      <el-table-column prop="address" label="地址" min-width="180" show-overflow-tooltip />
+      <el-table-column label="经度" width="120" align="center">
+        <template #default="{ row }">{{ row.longitude }}</template>
+      </el-table-column>
+      <el-table-column label="纬度" width="120" align="center">
+        <template #default="{ row }">{{ row.latitude }}</template>
+      </el-table-column>
+      <el-table-column prop="diameter" label="管径(mm)" width="100" align="center" />
+      <el-table-column prop="material" label="材质" width="100" align="center" />
+      <el-table-column prop="status" label="状态" width="90" align="center">
+        <template #default="{ row }">
+          <el-tag :type="row.status === '0' ? 'success' : row.status === '1' ? 'danger' : 'info'">
+            {{ getDictLabel(pointStatusDict, row.status) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="200" fixed="right" align="center" class-name="small-padding fixed-width">
+        <template #default="{ row }">
+          <el-button link type="primary" icon="Edit" @click="handleEdit(row)">修改</el-button>
+          <el-button link type="primary" icon="Connection" @click="handleViewDevices(row)">设备</el-button>
+          <el-button link type="danger" icon="Delete" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
 
-    <el-card class="table-card">
-      <el-table :data="tableData" border stripe v-loading="loading" style="width: 100%">
-        <el-table-column prop="pointCode" label="管点编码" min-width="130" />
-        <el-table-column prop="pointName" label="管点名称" min-width="150" />
-        <el-table-column prop="networkName" label="所属管线" min-width="150" />
-        <el-table-column prop="pointType" label="管点类型" width="110">
-          <template #default="{ row }">{{ getDictLabel(pointTypeDict, row.pointType) }}</template>
-        </el-table-column>
-        <el-table-column prop="address" label="地址" min-width="180" />
-        <el-table-column label="经度" width="110">
-          <template #default="{ row }">{{ row.longitude }}</template>
-        </el-table-column>
-        <el-table-column label="纬度" width="110">
-          <template #default="{ row }">{{ row.latitude }}</template>
-        </el-table-column>
-        <el-table-column prop="diameter" label="管径(mm)" width="100" />
-        <el-table-column prop="material" label="材质" width="100" />
-        <el-table-column prop="status" label="状态" width="90">
-          <template #default="{ row }">
-            <el-tag :type="row.status === '0' ? 'success' : row.status === '1' ? 'danger' : 'info'">
-              {{ getDictLabel(pointStatusDict, row.status) }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="220" fixed="right">
-          <template #default="{ row }">
-            <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
-            <el-button type="warning" size="small" @click="handleViewDevices(row)">设备</el-button>
-            <el-button 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"
-        :total="total"
-        layout="total, prev, pager, next, sizes"
-        :page-sizes="[10, 20, 50]"
-        @change="loadData"
-        style="margin-top: 16px; justify-content: flex-end"
-      />
-    </el-card>
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      v-model:page="pageNum"
+      v-model:limit="pageSize"
+      :page-sizes="[10, 20, 50]"
+      @pagination="loadData"
+    />
 
-    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="650px" destroy-on-close>
+    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="760px" append-to-body destroy-on-close>
       <el-form :model="formData" :rules="rules" ref="formRef" label-width="100px">
         <el-row :gutter="20">
           <el-col :span="12">
@@ -87,39 +89,33 @@
               <el-input v-model="formData.pointName" placeholder="请输入管点名称" />
             </el-form-item>
           </el-col>
-        </el-row>
-        <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="所属管线" prop="networkId">
-              <el-select v-model="formData.networkId" placeholder="请选择管线" style="width:100%">
+              <el-select v-model="formData.networkId" placeholder="请选择管线" style="width: 100%">
                 <el-option v-for="opt in networkOptions" :key="opt.networkId" :label="opt.networkName" :value="opt.networkId" />
               </el-select>
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="管点类型" prop="pointType">
-              <el-select v-model="formData.pointType" placeholder="请选择" style="width:100%">
+              <el-select v-model="formData.pointType" placeholder="请选择管点类型" style="width: 100%">
                 <el-option v-for="d in pointTypeDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
               </el-select>
             </el-form-item>
           </el-col>
-        </el-row>
-        <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="经度">
-              <el-input-number v-model="formData.longitude" :precision="10" :step="0.0001" placeholder="经度" style="width:100%" />
+              <el-input-number v-model="formData.longitude" :precision="10" :step="0.0001" controls-position="right" placeholder="经度" style="width: 100%" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="纬度">
-              <el-input-number v-model="formData.latitude" :precision="10" :step="0.0001" placeholder="纬度" style="width:100%" />
+              <el-input-number v-model="formData.latitude" :precision="10" :step="0.0001" controls-position="right" placeholder="纬度" style="width: 100%" />
             </el-form-item>
           </el-col>
-        </el-row>
-        <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="管径(mm)">
-              <el-input-number v-model="formData.diameter" :precision="2" :step="1" placeholder="管径" style="width:100%" />
+              <el-input-number v-model="formData.diameter" :precision="2" :step="1" controls-position="right" placeholder="管径" style="width: 100%" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
@@ -127,65 +123,69 @@
               <el-input v-model="formData.material" placeholder="请输入材质" />
             </el-form-item>
           </el-col>
-        </el-row>
-        <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="埋深(米)">
-              <el-input-number v-model="formData.depth" :precision="2" :step="0.1" placeholder="埋深" style="width:100%" />
+              <el-input-number v-model="formData.depth" :precision="2" :step="0.1" controls-position="right" placeholder="埋深" style="width: 100%" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="建设日期">
-              <el-date-picker v-model="formData.buildDate" type="date" placeholder="选择日期" style="width:100%" value-format="YYYY-MM-DD" />
+              <el-date-picker v-model="formData.buildDate" type="date" placeholder="请选择建设日期" style="width: 100%" value-format="YYYY-MM-DD" />
             </el-form-item>
           </el-col>
-        </el-row>
-        <el-form-item label="地址">
-          <el-input v-model="formData.address" placeholder="请输入地址/位置描述" />
-        </el-form-item>
-        <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="状态">
-              <el-select v-model="formData.status" style="width:100%">
+              <el-select v-model="formData.status" placeholder="请选择状态" style="width: 100%">
                 <el-option v-for="d in pointStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
               </el-select>
             </el-form-item>
           </el-col>
+          <el-col :span="24">
+            <el-form-item label="地址">
+              <el-input v-model="formData.address" placeholder="请输入地址/位置描述" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="备注">
+              <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
+            </el-form-item>
+          </el-col>
         </el-row>
-        <el-form-item label="备注">
-          <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
-        </el-form-item>
       </el-form>
       <template #footer>
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="handleSubmit">确定</el-button>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="handleSubmit">确 定</el-button>
+          <el-button @click="dialogVisible = false">取 消</el-button>
+        </div>
       </template>
     </el-dialog>
 
-    <el-dialog title="关联设备" v-model="deviceDialogVisible" width="800px" destroy-on-close>
+    <el-dialog title="关联设备" v-model="deviceDialogVisible" width="800px" append-to-body destroy-on-close>
       <div v-if="currentPoint" class="device-section">
         <div class="device-header">
           <span>管点「{{ currentPoint.pointName }}」关联的设备列表</span>
-          <el-button type="primary" size="small" @click="handleAddDevice">新增设备</el-button>
+          <el-button type="primary" plain icon="Plus" @click="handleAddDevice">新增设备</el-button>
         </div>
-        <el-table :data="deviceList" border stripe v-loading="deviceLoading" style="margin-top:12px">
-          <el-table-column prop="equipmentId" label="设备ID" width="150" />
-          <el-table-column prop="equipmentName" label="设备名称" min-width="150" />
+        <el-table :data="deviceList" v-loading="deviceLoading" style="margin-top: 12px">
+          <el-table-column prop="equipmentId" label="设备ID" width="150" show-overflow-tooltip />
+          <el-table-column prop="equipmentName" label="设备名称" min-width="150" show-overflow-tooltip />
           <el-table-column prop="equipmentType" label="设备类型" width="120" />
-          <el-table-column prop="status" label="状态" width="90">
+          <el-table-column prop="status" label="状态" width="90" align="center">
             <template #default="{ row: dev }">
               <el-tag :type="dev.status === '0' ? 'success' : 'danger'" size="small">{{ dev.status === '0' ? '正常' : '异常' }}</el-tag>
             </template>
           </el-table-column>
-          <el-table-column label="操作" width="100">
+          <el-table-column label="操作" width="100" align="center">
             <template #default="{ row: dev }">
-              <el-button type="danger" size="small" @click="handleDeleteDevice(dev)">移除</el-button>
+              <el-button link type="danger" icon="Delete" @click="handleDeleteDevice(dev)">移除</el-button>
             </template>
           </el-table-column>
         </el-table>
       </div>
       <template #footer>
-        <el-button @click="deviceDialogVisible = false">关闭</el-button>
+        <div class="dialog-footer">
+          <el-button @click="deviceDialogVisible = false">关 闭</el-button>
+        </div>
       </template>
     </el-dialog>
   </div>
@@ -201,7 +201,11 @@ import {
 } from '@/api/pipeNetwork/basic'
 import { getDicts } from '@/api/system/dict/data'
 
+defineOptions({ name: 'GasPipePoint' })
+
 const loading = ref(false)
+const showSearch = ref(true)
+const queryRef = ref(null)
 const tableData = ref([])
 const pageNum = ref(1)
 const pageSize = ref(10)
@@ -273,6 +277,7 @@ async function loadData() {
 
 function handleSearch() { pageNum.value = 1; loadData() }
 function handleReset() {
+  queryRef.value?.resetFields()
   Object.assign(searchForm, { pointName: '', pointCode: '', networkId: '', pointType: '', status: '' })
   handleSearch()
 }
@@ -322,7 +327,9 @@ async function handleSubmit() {
 }
 
 function handleDelete(row) {
-  ElMessageBox.confirm(`确认删除管点「${row.pointName}」?`, '提示', { type: 'warning' }).then(async () => {
+  ElMessageBox.confirm(`确认删除管点「${row.pointName}」?`, '提示', {
+    type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消'
+  }).then(async () => {
     try {
       await deleteGasPipePoint(row.pointId)
       ElMessage.success('删除成功')
@@ -351,7 +358,9 @@ function handleAddDevice() {
 }
 
 async function handleDeleteDevice(dev) {
-  ElMessageBox.confirm('确认移除该设备关联?', '提示', { type: 'warning' }).then(async () => {
+  ElMessageBox.confirm('确认移除该设备关联?', '提示', {
+    type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消'
+  }).then(async () => {
     try {
       await deletePipePointDeviceRel(dev.relId)
       ElMessage.success('移除成功')
@@ -367,8 +376,6 @@ onMounted(async () => {
 </script>
 
 <style scoped>
-.pipe-point-container { padding: 16px; }
-.search-card { margin-bottom: 16px; }
-.search-form { display: flex; flex-wrap: wrap; }
 .device-header { display: flex; justify-content: space-between; align-items: center; }
+.dialog-footer { display: flex; align-items: center; justify-content: flex-end; gap: 10px; }
 </style>

+ 235 - 163
src/views/subSystem/basic/Manhole.vue

@@ -1,158 +1,203 @@
 <template>
-  <div class="manhole-container">
-    <el-card class="search-card">
-      <el-form :inline="true" :model="searchForm">
-        <el-form-item label="窨井名称">
-          <el-input v-model="searchForm.manholeName" placeholder="请输入名称" clearable />
-        </el-form-item>
-        <el-form-item label="窨井编码">
-          <el-input v-model="searchForm.manholeCode" placeholder="请输入编码" clearable />
-        </el-form-item>
-        <el-form-item label="状态">
-          <el-select v-model="searchForm.status" placeholder="请选择" clearable style="width:160px">
-            <el-option v-for="d in pipeNetworkStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" @click="handleSearch">查询</el-button>
-          <el-button @click="handleReset">重置</el-button>
-          <el-button type="success" @click="handleAdd">新增</el-button>
-          <el-button @click="viewMode = viewMode === 'table' ? 'map' : 'table'">
-            {{ viewMode === 'table' ? '地图模式' : '列表模式' }}
-          </el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
+  <div class="app-container">
+    <el-form ref="queryRef" :inline="true" :model="searchForm" v-show="showSearch" label-width="80px">
+      <el-form-item label="窨井名称" prop="manholeName">
+        <el-input v-model="searchForm.manholeName" placeholder="请输入窨井名称" clearable style="width: 200px" @keyup.enter="handleSearch" />
+      </el-form-item>
+      <el-form-item label="窨井编码" prop="manholeCode">
+        <el-input v-model="searchForm.manholeCode" placeholder="请输入窨井编码" clearable style="width: 200px" @keyup.enter="handleSearch" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 200px">
+          <el-option v-for="d in pipeNetworkStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
+        <el-button icon="Refresh" @click="handleReset">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <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="success" plain :icon="viewMode === 'table' ? 'Location' : 'List'" @click="toggleView">
+          {{ viewMode === 'table' ? '地图模式' : '列表模式' }}
+        </el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="loadData" />
+    </el-row>
 
-    <el-card v-if="viewMode === 'table'" class="table-card">
-      <el-table :data="tableData" border stripe v-loading="loading">
-        <el-table-column prop="manholeCode" label="窨井编码" min-width="120" />
-        <el-table-column prop="manholeName" label="窨井名称" min-width="150" />
-        <el-table-column prop="networkId" label="关联管网" min-width="150">
+    <template v-if="viewMode === 'table'">
+      <el-table v-loading="loading" :data="tableData">
+        <el-table-column prop="manholeCode" label="窨井编码" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="manholeName" label="窨井名称" min-width="150" show-overflow-tooltip />
+        <el-table-column prop="networkId" label="关联管网" min-width="150" show-overflow-tooltip>
           <template #default="{ row }">
             <span v-if="row.networkId">{{ getNetworkName(row.networkId) }}</span>
             <el-tag v-else type="info" size="small">未关联</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="location" label="位置" min-width="180" />
-        <el-table-column prop="manholeDepth" label="深度(米)" width="90" />
-        <el-table-column prop="manholeShape" label="形状" width="80">
+        <el-table-column prop="location" label="位置" min-width="180" show-overflow-tooltip />
+        <el-table-column prop="manholeDepth" label="深度(m)" width="90" align="center" />
+        <el-table-column prop="manholeShape" label="形状" width="90" align="center">
           <template #default="{ row }">{{ getDictLabel(manholeShapeDict, row.manholeShape) }}</template>
         </el-table-column>
         <el-table-column prop="material" label="材质" width="100" />
-        <el-table-column prop="ownershipUnit" label="权属单位" min-width="150" />
-        <el-table-column prop="status" label="状态" width="90">
+        <el-table-column prop="ownershipUnit" label="权属单位" min-width="150" show-overflow-tooltip />
+        <el-table-column prop="status" label="状态" width="90" align="center">
           <template #default="{ row }">
             <el-tag :type="row.status === '0' ? 'success' : 'danger'">{{ getDictLabel(pipeNetworkStatusDict, row.status) }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="240" fixed="right">
+        <el-table-column label="操作" width="200" align="center" fixed="right" class-name="small-padding fixed-width">
           <template #default="{ row }">
-            <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
-            <el-button type="success" size="small" @click="handleDeviceDialog(row)">关联设备</el-button>
-            <el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
+            <el-button link type="primary" icon="Edit" @click="handleEdit(row)">修改</el-button>
+            <el-button link type="primary" icon="Connection" @click="handleDeviceDialog(row)">关联设备</el-button>
+            <el-button link type="danger" icon="Delete" @click="handleDelete(row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
-      <el-pagination
-        v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total"
-        layout="total, prev, pager, next, sizes" :page-sizes="[10, 20, 50]"
-        @change="loadData" style="margin-top: 16px; justify-content: flex-end"
+
+      <pagination
+        v-show="total > 0"
+        :total="total"
+        v-model:page="pageNum"
+        v-model:limit="pageSize"
+        :page-sizes="[10, 20, 50]"
+        @pagination="loadData"
       />
-    </el-card>
+    </template>
 
-    <el-card v-else class="map-card">
+    <div v-else class="map-card">
       <div id="manholeMap" style="width: 100%; height: 600px"></div>
-      <div v-if="mapError" style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#f56c6c;font-size:14px;">{{ mapError }}</div>
-    </el-card>
+      <div v-if="mapError" class="map-error">{{ mapError }}</div>
+    </div>
 
-    <el-dialog title="关联监测设备" v-model="deviceDialogVisible" width="950px" destroy-on-close>
-      <el-form label-width="80px">
-        <el-form-item label="当前窨井">
-          <span>{{ currentManhole.manholeName || '-' }}</span>
-        </el-form-item>
-        <el-form-item label="选择设备">
-          <el-select
-            v-model="bindForm.equipmentId"
-            filterable
-            clearable
-            placeholder="请选择要关联的设备"
-            style="width: 100%"
-          >
-            <el-option
-              v-for="item in unboundEquipmentOptions"
-              :key="item.equipmentId"
-              :label="`${item.equipmentName}(${item.equipmentCode})`"
-              :value="item.equipmentId"
-            />
-          </el-select>
-        </el-form-item>
+    <el-dialog title="关联监测设备" v-model="deviceDialogVisible" width="950px" append-to-body destroy-on-close>
+      <el-form label-width="90px">
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="当前窨井">
+              <span>{{ currentManhole.manholeName || '-' }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="选择设备">
+              <el-select v-model="bindForm.equipmentId" filterable clearable placeholder="请选择要关联的设备" style="width: 100%">
+                <el-option
+                  v-for="item in unboundEquipmentOptions"
+                  :key="item.equipmentId"
+                  :label="`${item.equipmentName}(${item.equipmentCode})`"
+                  :value="item.equipmentId"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
       </el-form>
-      <div style="margin-bottom: 12px">
-        <el-button type="primary" @click="submitBind">添加关联</el-button>
-      </div>
-      <el-table :data="deviceList" border stripe v-loading="deviceLoading">
-        <el-table-column label="设备编码" prop="equipmentCode" min-width="140" />
+      <el-row :gutter="10" class="mb8">
+        <el-col :span="1.5">
+          <el-button type="primary" plain icon="Plus" :disabled="!bindForm.equipmentId" @click="submitBind">添加关联</el-button>
+        </el-col>
+      </el-row>
+      <el-table :data="deviceList" v-loading="deviceLoading">
+        <el-table-column label="设备编码" prop="equipmentCode" min-width="140" show-overflow-tooltip />
         <el-table-column label="设备名称" prop="equipmentName" min-width="180" show-overflow-tooltip />
-        <el-table-column label="设备型号" prop="equipmentModel" min-width="120" />
+        <el-table-column label="设备型号" prop="equipmentModel" min-width="120" show-overflow-tooltip />
         <el-table-column label="制造商" prop="manufacturer" min-width="140" show-overflow-tooltip />
         <el-table-column label="设备位置" prop="equipmentLocation" min-width="180" show-overflow-tooltip />
-        <el-table-column label="操作" align="center" width="100">
+        <el-table-column label="操作" align="center" width="100" fixed="right">
           <template #default="{ row: dev }">
             <el-button link type="danger" icon="Close" @click="handleUnbind(dev)">解绑</el-button>
           </template>
         </el-table-column>
       </el-table>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="deviceDialogVisible = false">关 闭</el-button>
+        </div>
+      </template>
     </el-dialog>
 
-    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="650px" destroy-on-close>
-      <el-form :model="formData" :rules="rules" ref="formRef" label-width="100px">
-        <el-form-item label="窨井编码" prop="manholeCode">
-          <el-input v-model="formData.manholeCode" placeholder="请输入窨井编码" />
-        </el-form-item>
-        <el-form-item label="窨井名称" prop="manholeName">
-          <el-input v-model="formData.manholeName" placeholder="请输入窨井名称" />
-        </el-form-item>
-        <el-form-item label="关联管网">
-          <el-select v-model="formData.networkId" placeholder="请选择管网" clearable style="width: 100%">
-            <el-option v-for="n in networkOptions" :key="n.networkId" :label="`${n.networkName}(${n.networkCode})`" :value="n.networkId" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="位置描述" prop="location">
-          <el-input v-model="formData.location" placeholder="请输入位置描述" />
-        </el-form-item>
-        <el-form-item label="经度" prop="longitude">
-          <el-input-number v-model="formData.longitude" :precision="10" :step="0.0001" style="width: 100%" />
-        </el-form-item>
-        <el-form-item label="纬度" prop="latitude">
-          <el-input-number v-model="formData.latitude" :precision="10" :step="0.0001" style="width: 100%" />
-        </el-form-item>
-        <el-form-item label="深度(米)">
-          <el-input-number v-model="formData.manholeDepth" :precision="2" :step="0.1" style="width: 100%" />
-        </el-form-item>
-        <el-form-item label="形状">
-          <el-select v-model="formData.manholeShape">
-            <el-option v-for="d in manholeShapeDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="材质">
-          <el-input v-model="formData.material" placeholder="请输入材质" />
-        </el-form-item>
-        <el-form-item label="权属单位">
-          <el-input v-model="formData.ownershipUnit" placeholder="请输入权属单位" />
-        </el-form-item>
-        <el-form-item label="状态">
-          <el-select v-model="formData.status">
-            <el-option v-for="d in pipeNetworkStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="备注">
-          <el-input v-model="formData.remark" type="textarea" :rows="3" />
-        </el-form-item>
+    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="760px" append-to-body destroy-on-close>
+      <el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="窨井编码" prop="manholeCode">
+              <el-input v-model="formData.manholeCode" placeholder="请输入窨井编码" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="窨井名称" prop="manholeName">
+              <el-input v-model="formData.manholeName" placeholder="请输入窨井名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="关联管网">
+              <el-select v-model="formData.networkId" placeholder="请选择管网" clearable style="width: 100%">
+                <el-option v-for="n in networkOptions" :key="n.networkId" :label="`${n.networkName}(${n.networkCode})`" :value="n.networkId" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="位置描述" prop="location">
+              <el-input v-model="formData.location" placeholder="请输入位置描述" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="经度" prop="longitude">
+              <el-input-number v-model="formData.longitude" :precision="10" :step="0.0001" controls-position="right" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="纬度" prop="latitude">
+              <el-input-number v-model="formData.latitude" :precision="10" :step="0.0001" controls-position="right" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="深度(m)">
+              <el-input-number v-model="formData.manholeDepth" :precision="2" :step="0.1" controls-position="right" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="形状">
+              <el-select v-model="formData.manholeShape" placeholder="请选择形状" style="width: 100%">
+                <el-option v-for="d in manholeShapeDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="材质">
+              <el-input v-model="formData.material" placeholder="请输入材质" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="权属单位">
+              <el-input v-model="formData.ownershipUnit" placeholder="请输入权属单位" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="状态">
+              <el-select v-model="formData.status" placeholder="请选择状态" style="width: 100%">
+                <el-option v-for="d in pipeNetworkStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="备注">
+              <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
+            </el-form-item>
+          </el-col>
+        </el-row>
       </el-form>
       <template #footer>
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="handleSubmit">确定</el-button>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="handleSubmit">确 定</el-button>
+          <el-button @click="dialogVisible = false">取 消</el-button>
+        </div>
       </template>
     </el-dialog>
   </div>
@@ -167,23 +212,33 @@ import {
   getEquipmentByTopLevelType, getPipeNetworkOptions
 } from '@/api/pipeNetwork/basic'
 import { getDicts } from '@/api/system/dict/data'
+// 台账存 WGS84,百度地图是 BD-09,不转换会偏约 1 公里
+import { convertCoord } from '@/utils/coordTransform'
+
+defineOptions({ name: 'Manhole' })
 
 const loading = ref(false)
+const showSearch = ref(true)
 const tableData = ref([])
 const pageNum = ref(1)
 const pageSize = ref(10)
 const total = ref(0)
 const viewMode = ref('table')
 
+const queryRef = ref(null)
 const searchForm = reactive({ manholeName: '', manholeCode: '', status: '' })
 const dialogVisible = ref(false)
 const dialogTitle = ref('新增')
 const formRef = ref(null)
-const formData = reactive({
-  manholeId: '', manholeCode: '', manholeName: '', networkId: '',
-  location: '', longitude: null, latitude: null, manholeDepth: null,
-  manholeShape: '', material: '', ownershipUnit: '', status: '0', remark: ''
-})
+
+function defaultForm() {
+  return {
+    manholeId: '', manholeCode: '', manholeName: '', networkId: '',
+    location: '', longitude: null, latitude: null, manholeDepth: null,
+    manholeShape: '', material: '', ownershipUnit: '', status: '0', remark: ''
+  }
+}
+const formData = reactive(defaultForm())
 const rules = {
   manholeCode: [{ required: true, message: '请输入窨井编码', trigger: 'blur' }],
   manholeName: [{ required: true, message: '请输入窨井名称', trigger: 'blur' }],
@@ -206,6 +261,7 @@ const unboundEquipmentOptions = computed(() => {
   const boundIds = deviceList.value.map(d => d.equipmentId)
   return equipmentOptions.value.filter(e => !boundIds.includes(e.equipmentId))
 })
+
 function getDictLabel(dict, value) {
   const item = dict.find(d => d.dictValue === value)
   return item ? item.dictLabel : value
@@ -235,7 +291,14 @@ async function loadData() {
 }
 
 function handleSearch() { pageNum.value = 1; loadData() }
-function handleReset() { Object.assign(searchForm, { manholeName: '', manholeCode: '', status: '' }); handleSearch() }
+function handleReset() {
+  queryRef.value?.resetFields()
+  Object.assign(searchForm, { manholeName: '', manholeCode: '', status: '' })
+  handleSearch()
+}
+function toggleView() {
+  viewMode.value = viewMode.value === 'table' ? 'map' : 'table'
+}
 async function loadNetworkOptions() {
   if (networkOptions.value.length > 0) return
   try {
@@ -244,12 +307,19 @@ async function loadNetworkOptions() {
   } catch (e) { console.error(e) }
 }
 function handleAdd() {
-  dialogTitle.value = '新增'
-  Object.assign(formData, { manholeId: '', manholeCode: '', manholeName: '', networkId: '', location: '', longitude: null, latitude: null, manholeDepth: null, manholeShape: '', material: '', ownershipUnit: '', status: '0', remark: '' })
+  dialogTitle.value = '新增窨井'
+  Object.assign(formData, defaultForm())
+  dialogVisible.value = true
+  loadNetworkOptions()
+}
+function handleEdit(row) {
+  dialogTitle.value = '修改窨井'
+  const base = defaultForm()
+  Object.keys(base).forEach(k => { base[k] = row[k] ?? base[k] })
+  Object.assign(formData, base)
   dialogVisible.value = true
   loadNetworkOptions()
 }
-function handleEdit(row) { dialogTitle.value = '编辑'; Object.assign(formData, row); dialogVisible.value = true; loadNetworkOptions() }
 async function handleSubmit() {
   const valid = await formRef.value.validate().catch(() => false)
   if (!valid) return
@@ -263,7 +333,9 @@ async function handleSubmit() {
   }
 }
 function handleDelete(row) {
-  ElMessageBox.confirm('确认删除该窨井信息?', '提示', { type: 'warning' }).then(async () => {
+  ElMessageBox.confirm(`确认删除窨井「${row.manholeName}」?`, '提示', {
+    type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消'
+  }).then(async () => {
     await deleteManhole(row.manholeId); ElMessage.success('删除成功'); loadData()
   }).catch(() => {})
 }
@@ -280,31 +352,19 @@ async function loadDeviceList() {
   try {
     const res = await getManholeDevices(currentManhole.manholeId)
     const raw = res.data || []
-    deviceList.value = raw.map(r => ({
-      relId: r.relId,
-      equipmentId: r.equipmentId,
-      equipmentName: r.equipmentName,
-      equipmentType: r.equipmentType,
-      equipmentCode: '',
-      equipmentModel: '',
-      manufacturer: '',
-      equipmentLocation: ''
-    }))
-    if (raw.length > 0) {
-      const eqMap = new Map(equipmentOptions.value.map(e => [e.equipmentId, e]))
-      deviceList.value = raw.map(r => {
-        const full = eqMap.get(r.equipmentId)
-        return {
-          relId: r.relId,
-          equipmentId: r.equipmentId,
-          equipmentName: r.equipmentName || (full ? full.equipmentName : ''),
-          equipmentCode: full ? full.equipmentCode : '',
-          equipmentModel: full ? full.equipmentModel : '',
-          manufacturer: full ? full.manufacturer : '',
-          equipmentLocation: full ? full.equipmentLocation : ''
-        }
-      })
-    }
+    const eqMap = new Map(equipmentOptions.value.map(e => [e.equipmentId, e]))
+    deviceList.value = raw.map(r => {
+      const full = eqMap.get(r.equipmentId)
+      return {
+        relId: r.relId,
+        equipmentId: r.equipmentId,
+        equipmentName: r.equipmentName || (full ? full.equipmentName : ''),
+        equipmentCode: full ? full.equipmentCode : '',
+        equipmentModel: full ? full.equipmentModel : '',
+        manufacturer: full ? full.manufacturer : '',
+        equipmentLocation: full ? full.equipmentLocation : ''
+      }
+    })
   } catch (e) { console.error(e) } finally { deviceLoading.value = false }
 }
 async function loadEquipmentOptions() {
@@ -329,7 +389,9 @@ async function submitBind() {
   } catch (e) { console.error(e) }
 }
 function handleUnbind(dev) {
-  ElMessageBox.confirm('确认解除该设备关联?', '提示', { type: 'warning' }).then(async () => {
+  ElMessageBox.confirm('确认解除该设备关联?', '提示', {
+    type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消'
+  }).then(async () => {
     await deleteManholeDeviceRel(dev.relId)
     ElMessage.success('解绑成功')
     loadDeviceList()
@@ -361,8 +423,8 @@ async function initMap() {
     const BMapGL = await waitForBMapGL()
     mapInstance = new BMapGL.Map(el, { enableMapClick: true })
     const center = tableData.value.length > 0 && tableData.value[0].longitude
-      ? [Number(tableData.value[0].longitude), Number(tableData.value[0].latitude)]
-      : [110.3965, 28.4638]
+      ? convertCoord(Number(tableData.value[0].longitude), Number(tableData.value[0].latitude))
+      : convertCoord(110.3965, 28.4638)
     mapInstance.centerAndZoom(new BMapGL.Point(center[0], center[1]), 15)
     mapInstance.enableScrollWheelZoom(true)
     mapInitialized = true
@@ -381,8 +443,7 @@ function renderMarkers() {
   if (data.length === 0) return
 
   data.forEach(m => {
-    const lng = Number(m.longitude)
-    const lat = Number(m.latitude)
+    const [lng, lat] = convertCoord(Number(m.longitude), Number(m.latitude))
     const pt = new BMapGL.Point(lng, lat)
     const marker = new BMapGL.Marker(pt)
     mapInstance.addOverlay(marker)
@@ -420,7 +481,18 @@ onBeforeUnmount(() => {
 </script>
 
 <style scoped>
-.manhole-container { padding: 16px; }
-.search-card { margin-bottom: 16px; }
-.map-card { position: relative; }
+.map-card {
+  position: relative;
+  padding: 16px;
+  background: #fff;
+  border-radius: 4px;
+}
+.map-error {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  color: #f56c6c;
+  font-size: 14px;
+}
 </style>

+ 149 - 191
src/views/subSystem/basic/PipeNetwork.vue

@@ -1,159 +1,141 @@
 <template>
-  <div class="pipe-network-container">
-    <el-card class="search-card">
-      <el-form :inline="true" :model="searchForm" class="search-form">
-        <el-form-item label="设施名称">
-          <el-input v-model="searchForm.networkName" placeholder="请输入设施名称" clearable />
-        </el-form-item>
-        <el-form-item label="管养单位">
-          <el-input v-model="searchForm.managementUnit" placeholder="请输入管养单位" clearable />
-        </el-form-item>
-        <el-form-item label="等级">
-          <el-select v-model="searchForm.networkLevel" placeholder="请选择" clearable style="width:160px">
-            <el-option v-for="d in pipeNetworkLevelDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="材质">
-          <el-input v-model="searchForm.material" placeholder="请输入材质" clearable style="width:160px" />
-        </el-form-item>
-        <el-form-item label="状态">
-          <el-select v-model="searchForm.status" placeholder="请选择" clearable style="width:160px">
-            <el-option v-for="d in pipeNetworkStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" @click="handleSearch">查询</el-button>
-          <el-button @click="handleReset">重置</el-button>
-          <el-button type="success" @click="handleAdd">新增</el-button>
-          <el-button type="warning" @click="handleExport">导出</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
-
-    <el-card class="table-card">
-      <el-table :data="tableData" border stripe v-loading="loading" style="width: 100%">
-        <el-table-column prop="networkCode" label="设施编码" min-width="120" />
-        <el-table-column prop="networkName" label="设施名称" min-width="150" />
-        <el-table-column prop="networkType" label="设施类型" width="100">
-          <template #default="{ row }">{{ getDictLabel(pipeNetworkTypeDict, row.networkType) }}</template>
-        </el-table-column>
-        <el-table-column prop="networkLevel" label="等级" width="90">
-          <template #default="{ row }">{{ getDictLabel(pipeNetworkLevelDict, row.networkLevel) }}</template>
-        </el-table-column>
-        <el-table-column prop="managementUnit" label="管养单位" min-width="150" />
-        <el-table-column prop="material" label="材质" width="100" />
-        <el-table-column prop="length" label="长度(米)" width="100" />
-        <el-table-column prop="diameter" label="管径(毫米)" width="110" />
-        <el-table-column prop="location" label="位置" min-width="180" />
-        <el-table-column prop="buildDate" label="建设日期" width="120" />
-        <el-table-column prop="status" label="状态" width="90">
-          <template #default="{ row }">
-            <el-tag :type="statusTagType(row.status)">{{ getDictLabel(pipeNetworkStatusDict, row.status) }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="180" fixed="right">
-          <template #default="{ row }">
-            <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
-            <el-button 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"
-        :total="total"
-        layout="total, prev, pager, next, sizes"
-        :page-sizes="[10, 20, 50]"
-        @change="loadData"
-        style="margin-top: 16px; justify-content: flex-end"
-      />
-    </el-card>
-
-    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="650px" destroy-on-close>
-      <el-form :model="formData" :rules="rules" ref="formRef" label-width="100px">
-        <el-form-item label="设施编码" prop="networkCode">
-          <el-input v-model="formData.networkCode" placeholder="请输入设施编码" />
-        </el-form-item>
-        <el-form-item label="设施名称" prop="networkName">
-          <el-input v-model="formData.networkName" placeholder="请输入设施名称" />
-        </el-form-item>
-        <el-form-item label="等级">
-          <el-select v-model="formData.networkLevel">
-            <el-option v-for="d in pipeNetworkLevelDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="管养单位">
-          <el-input v-model="formData.managementUnit" placeholder="请输入管养单位" />
-        </el-form-item>
-        <el-form-item label="位置描述">
-          <el-input v-model="formData.location" placeholder="请输入位置描述" />
-        </el-form-item>
-        <el-form-item label="材质">
-          <el-input v-model="formData.material" placeholder="请输入材质" />
-        </el-form-item>
-        <el-form-item label="长度(米)">
-          <el-input-number v-model="formData.length" :precision="2" :step="1" :min="0" placeholder="请输入长度" style="width: 100%" />
-        </el-form-item>
-        <el-form-item label="管径(毫米)">
-          <el-input-number v-model="formData.diameter" :precision="0" :step="1" :min="0" placeholder="请输入管径" style="width: 100%" />
-        </el-form-item>
-        <el-form-item label="建设日期">
-          <el-date-picker v-model="formData.buildDate" type="date" placeholder="请选择建设日期" value-format="YYYY-MM-DD" style="width: 100%" />
-        </el-form-item>
-        <el-form-item label="经度">
-          <el-input-number v-model="formData.longitude" :precision="10" :step="0.0001" placeholder="请输入经度" style="width: 100%" />
-        </el-form-item>
-        <el-form-item label="纬度">
-          <el-input-number v-model="formData.latitude" :precision="10" :step="0.0001" placeholder="请输入纬度" style="width: 100%" />
-        </el-form-item>
-        <el-form-item label="状态">
-          <el-select v-model="formData.status" placeholder="请选择">
-            <el-option v-for="d in pipeNetworkStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="备注">
-          <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
-        </el-form-item>
-      </el-form>
+  <div class="app-container">
+    <!-- 查询条件 -->
+    <el-form ref="queryRef" :model="searchForm" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item label="设施名称" prop="networkName">
+        <el-input v-model="searchForm.networkName" placeholder="请输入设施名称" clearable style="width: 180px"
+          @keyup.enter="handleSearch" />
+      </el-form-item>
+      <el-form-item label="管养单位" prop="managementUnit">
+        <el-input v-model="searchForm.managementUnit" placeholder="请输入管养单位" clearable style="width: 180px"
+          @keyup.enter="handleSearch" />
+      </el-form-item>
+      <el-form-item label="等级" prop="networkLevel">
+        <el-select v-model="searchForm.networkLevel" placeholder="请选择" clearable style="width: 160px">
+          <el-option v-for="d in pipeNetworkLevelDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="材质" prop="material">
+        <el-input v-model="searchForm.material" placeholder="请输入材质" clearable style="width: 160px"
+          @keyup.enter="handleSearch" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="searchForm.status" placeholder="请选择" clearable style="width: 160px">
+          <el-option v-for="d in pipeNetworkStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
+        <el-button icon="Refresh" @click="handleReset">重置</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" @click="handleExport">导出</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="loadData"></right-toolbar>
+    </el-row>
+
+    <!-- 管网清单 -->
+    <el-table ref="tableRef" v-loading="loading" :data="tableData" row-key="networkId" style="width: 100%"
+      @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="50" align="center" />
+      <el-table-column prop="networkCode" label="设施编码" min-width="120" show-overflow-tooltip />
+      <el-table-column prop="networkName" label="设施名称" min-width="150" show-overflow-tooltip />
+      <el-table-column prop="networkType" label="设施类型" width="100" align="center">
+        <template #default="{ row }">{{ getDictLabel(pipeNetworkTypeDict, row.networkType) }}</template>
+      </el-table-column>
+      <el-table-column prop="networkLevel" label="等级" width="90" align="center">
+        <template #default="{ row }">{{ getDictLabel(pipeNetworkLevelDict, row.networkLevel) }}</template>
+      </el-table-column>
+      <el-table-column prop="managementUnit" label="管养单位" min-width="150" show-overflow-tooltip />
+      <el-table-column prop="material" label="材质" width="100" align="center" />
+      <el-table-column prop="length" label="长度(m)" width="100" align="right" />
+      <el-table-column prop="diameter" label="管径(mm)" width="110" align="right" />
+      <el-table-column prop="location" label="位置" min-width="180" show-overflow-tooltip />
+      <el-table-column prop="buildDate" label="建设日期" width="120" align="center" />
+      <el-table-column label="状态" width="90" align="center">
+        <template #default="{ row }">
+          <el-tag size="small" :type="statusTagType(row.status)">
+            {{ getDictLabel(pipeNetworkStatusDict, row.status) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="90" align="center" fixed="right">
+        <template #default="{ row }">
+          <el-button link type="primary" icon="View" @click="handleDetail(row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize"
+      @pagination="loadData" />
+
+    <!-- 详情 -->
+    <el-drawer v-model="detailVisible" title="管网详情" size="640px" append-to-body destroy-on-close>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="设施编码">{{ detailData.networkCode || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="设施名称">{{ detailData.networkName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="设施类型">
+          {{ getDictLabel(pipeNetworkTypeDict, detailData.networkType) || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="等级">
+          {{ getDictLabel(pipeNetworkLevelDict, detailData.networkLevel) || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="管养单位">{{ detailData.managementUnit || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="材质">{{ detailData.material || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="长度(m)">{{ detailData.length ?? '-' }}</el-descriptions-item>
+        <el-descriptions-item label="管径(mm)">{{ detailData.diameter ?? '-' }}</el-descriptions-item>
+        <el-descriptions-item label="建设日期">{{ detailData.buildDate || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag size="small" :type="statusTagType(detailData.status)">
+            {{ getDictLabel(pipeNetworkStatusDict, detailData.status) || '-' }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="位置" :span="2">{{ detailData.location || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="起点经度">{{ detailData.startLongitude ?? '-' }}</el-descriptions-item>
+        <el-descriptions-item label="起点纬度">{{ detailData.startLatitude ?? '-' }}</el-descriptions-item>
+        <el-descriptions-item label="终点经度">{{ detailData.endLongitude ?? '-' }}</el-descriptions-item>
+        <el-descriptions-item label="终点纬度">{{ detailData.endLatitude ?? '-' }}</el-descriptions-item>
+        <el-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间" :span="2">{{ detailData.createTime || '-' }}</el-descriptions-item>
+      </el-descriptions>
       <template #footer>
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="handleSubmit">确定</el-button>
+        <div class="dialog-footer">
+          <el-button @click="detailVisible = false">关 闭</el-button>
+        </div>
       </template>
-    </el-dialog>
+    </el-drawer>
   </div>
 </template>
 
 <script setup>
 import { ref, reactive, onMounted } from 'vue'
 import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
-import { getPipeNetworkPage, getPipeNetworkById, addPipeNetwork, updatePipeNetwork, deletePipeNetwork, exportPipeNetwork } from '@/api/pipeNetwork/basic'
+import { getPipeNetworkPage, exportPipeNetwork } from '@/api/pipeNetwork/basic'
 import { getDicts } from '@/api/system/dict/data'
 import { saveAs } from 'file-saver'
 
+defineOptions({ name: 'GasPipeNetwork' })
+
 const loading = ref(false)
+const showSearch = ref(true)
 const tableData = ref([])
 const pageNum = ref(1)
 const pageSize = ref(10)
 const total = ref(0)
+const queryRef = ref(null)
+const tableRef = ref(null)
+const selectedIds = ref([])
 
 const searchForm = reactive({
   networkName: '', managementUnit: '', networkLevel: '', networkType: '', material: '', status: ''
 })
 
-const dialogVisible = ref(false)
-const dialogTitle = ref('新增')
-const formRef = ref(null)
-const formData = reactive({
-  networkId: '', networkCode: '', networkName: '', networkType: 'GAS',
-  networkLevel: '', managementUnit: '', location: '', material: '',
-  length: null, diameter: null, buildDate: null,
-  longitude: null, latitude: null, status: '0', remark: ''
-})
-
-const rules = {
-  networkCode: [{ required: true, message: '请输入设施编码', trigger: 'blur' }],
-  networkName: [{ required: true, message: '请输入设施名称', trigger: 'blur' }]
-}
+const detailVisible = ref(false)
+const detailData = ref({})
 
 const pipeNetworkStatusDict = ref([])
 const pipeNetworkLevelDict = ref([])
@@ -164,7 +146,7 @@ function getDictLabel(dict, value) {
   return item ? item.dictLabel : value
 }
 
-// 管网状态颜色:0 绿(正常)、1 橙(维护中)、2 红(异常),与管网信息查询页口径一致(#55)
+// 管网状态颜色:0 绿(正常)、1 橙(维护中)、2 红(异常)
 function statusTagType(status) {
   return { '0': 'success', '1': 'warning', '2': 'danger' }[String(status)] || 'info'
 }
@@ -186,6 +168,9 @@ async function loadData() {
     const res = await getPipeNetworkPage(pageNum.value, pageSize.value, searchForm)
     tableData.value = res.data.records || []
     total.value = res.data.total || 0
+    // 列表数据变化后清空勾选,避免导出范围与界面不一致
+    tableRef.value?.clearSelection()
+    selectedIds.value = []
   } catch (e) {
     console.error(e)
   } finally {
@@ -199,83 +184,56 @@ function handleSearch() {
 }
 
 function handleReset() {
+  queryRef.value?.resetFields()
   Object.assign(searchForm, { networkName: '', managementUnit: '', networkLevel: '', networkType: '', material: '', status: '' })
   handleSearch()
 }
 
-function handleAdd() {
-  dialogTitle.value = '新增'
-  Object.assign(formData, { networkId: '', networkCode: '', networkName: '', networkType: 'GAS', networkLevel: '', managementUnit: '', location: '', material: '', length: null, diameter: null, buildDate: null, longitude: null, latitude: null, status: '0', remark: '' })
-  dialogVisible.value = true
+function handleSelectionChange(selection) {
+  selectedIds.value = selection.map(item => item.networkId)
 }
 
-function toDateStr(val) {
-  // 转为 yyyy-MM-dd 字符串,供 value-format="YYYY-MM-DD" 的 date-picker 回显
-  if (!val) return ''
-  if (Array.isArray(val)) {
-    return val[0] + '-' + String(val[1]).padStart(2, '0') + '-' + String(val[2]).padStart(2, '0')
-  }
-  const s = String(val)
-  const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/)
-  if (m) return m[0]
-  const d = new Date(s)
-  if (!isNaN(d.getTime())) {
-    return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
-  }
-  return ''
+function handleDetail(row) {
+  detailData.value = { ...row }
+  detailVisible.value = true
 }
 
-function handleEdit(row) {
-  dialogTitle.value = '编辑'
-  Object.assign(formData, { ...row, buildDate: toDateStr(row.buildDate), networkType: row.networkType || 'GAS' })
-  dialogVisible.value = true
-}
-
-async function handleSubmit() {
-  const valid = await formRef.value.validate().catch(() => false)
-  if (!valid) return
+async function doExport(networkIds) {
+  const loadingInstance = ElLoading.service({ text: '正在导出数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' })
   try {
-    if (formData.networkId) {
-      await updatePipeNetwork(formData)
-      ElMessage.success('修改成功')
-    } else {
-      await addPipeNetwork(formData)
-      ElMessage.success('新增成功')
+    const params = { ...searchForm }
+    // 未勾选时不传该参数,避免后端按空主键过滤
+    if (networkIds.length) {
+      params.networkIds = networkIds.join(',')
     }
-    dialogVisible.value = false
-    loadData()
-  } catch (e) {
-    console.error(e)
-  }
-}
-
-function handleDelete(row) {
-  ElMessageBox.confirm('确认删除该管网信息?', '提示', { type: 'warning' }).then(async () => {
-    await deletePipeNetwork(row.networkId)
-    ElMessage.success('删除成功')
-    loadData()
-  }).catch(() => {})
-}
-
-async function handleExport() {
-  const loading = ElLoading.service({ text: '正在导出数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' })
-  try {
-    const blob = await exportPipeNetwork({ networkName: searchForm.networkName, status: searchForm.status })
+    const blob = await exportPipeNetwork(params)
     saveAs(new Blob([blob], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), `燃气管网数据_${new Date().toISOString().slice(0, 10)}.xlsx`)
     ElMessage.success('导出成功')
   } catch (e) {
     console.error(e)
     ElMessage.error('导出失败,请稍后重试')
   } finally {
-    loading.close()
+    loadingInstance.close()
   }
 }
 
+function handleExport() {
+  const networkIds = selectedIds.value
+  const tip = networkIds.length
+    ? `是否确认导出选中的 ${networkIds.length} 条数据项?`
+    : '当前未勾选数据,是否确认导出所有符合条件的数据项?'
+  ElMessageBox.confirm(tip, '提示', {
+    type: 'warning',
+    confirmButtonText: '确定',
+    cancelButtonText: '取消'
+  }).then(() => doExport(networkIds)).catch(() => {})
+}
+
 onMounted(async () => { await loadDicts(); loadData() })
 </script>
 
 <style scoped>
-.pipe-network-container { padding: 16px; }
-.search-card { margin-bottom: 16px; }
-.search-form { display: flex; flex-wrap: wrap; }
+.dialog-footer {
+  text-align: right;
+}
 </style>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 599 - 256
src/views/subSystem/basic/RepairRecord.vue


+ 544 - 0
src/views/subSystem/gas/gasBasicadata/gasPipePoint.vue

@@ -0,0 +1,544 @@
+<!--燃气管点台账:数据来自 gas_pipe_point,仅燃气口径-->
+<template>
+  <div class="app-container">
+    <!-- 查询条件 -->
+    <el-form ref="queryRef" :model="queryParams" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item label="管点编码" prop="pointCode">
+        <el-input v-model="queryParams.pointCode" placeholder="请输入管点编码" clearable style="width: 160px"
+          @keyup.enter="handleQuery" />
+      </el-form-item>
+      <el-form-item label="管点名称" prop="pointName">
+        <el-input v-model="queryParams.pointName" placeholder="请输入管点名称" clearable style="width: 160px"
+          @keyup.enter="handleQuery" />
+      </el-form-item>
+      <el-form-item label="所属管线" prop="networkId">
+        <el-select v-model="queryParams.networkId" placeholder="请选择" clearable filterable style="width: 200px">
+          <el-option v-for="item in networkOptions" :key="item.networkId"
+            :label="item.networkName || item.networkCode" :value="item.networkId" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="管点类型" prop="pointType">
+        <el-select v-model="queryParams.pointType" placeholder="请选择" clearable filterable allow-create
+          style="width: 150px">
+          <el-option v-for="item in pointTypeOptions" :key="item" :label="item" :value="item" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 130px">
+          <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作栏 -->
+    <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="success" plain icon="Edit" :disabled="single"
+          @click="handleUpdate()">修改</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="danger" plain icon="Delete" :disabled="multiple"
+          @click="handleDelete()">
+          删除
+        </el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 管点清单 -->
+    <el-table v-loading="loading" :data="pointList" @selection-change="handleSelectionChange"
+      @row-dblclick="handleView">
+      <el-table-column type="selection" width="50" align="center" />
+      <el-table-column label="管点编码" prop="pointCode" min-width="120" show-overflow-tooltip />
+      <el-table-column label="管点名称" prop="pointName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="所属管线" min-width="150" show-overflow-tooltip>
+        <template #default="{ row }">{{ networkNameOf(row) }}</template>
+      </el-table-column>
+      <el-table-column label="管点类型" prop="pointType" width="110" align="center">
+        <template #default="{ row }">{{ blankOf(row.pointType) }}</template>
+      </el-table-column>
+      <el-table-column label="材质" prop="material" width="90" align="center">
+        <template #default="{ row }">{{ blankOf(row.material) }}</template>
+      </el-table-column>
+      <el-table-column label="管径(mm)" prop="diameter" width="100" align="right">
+        <template #default="{ row }">{{ numberOf(row.diameter, 0) }}</template>
+      </el-table-column>
+      <el-table-column label="埋深(m)" prop="depth" width="90" align="right">
+        <template #default="{ row }">{{ numberOf(row.depth, 2) }}</template>
+      </el-table-column>
+      <el-table-column label="经纬度" min-width="180">
+        <template #default="{ row }">
+          <span v-if="hasCoordinate(row)">{{ coordinateOf(row) }}</span>
+          <span v-else class="text-muted">未录入</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="建设日期" prop="buildDate" width="110" align="center">
+        <template #default="{ row }">{{ blankOf(row.buildDate) }}</template>
+      </el-table-column>
+      <el-table-column label="状态" width="90" align="center">
+        <template #default="{ row }">
+          <el-tag size="small" :type="statusTagType(row.status)">{{ statusLabel(row.status) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="170" align="center" fixed="right">
+        <template #default="{ row }">
+          <el-button link type="primary" @click="handleView(row)">详情</el-button>
+          <el-button link type="primary" @click="handleUpdate(row)">修改</el-button>
+          <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
+      v-model:limit="queryParams.pageSize" @pagination="getList" />
+
+    <!-- 新增 / 修改 -->
+    <el-dialog v-model="open" :title="title" width="680px" append-to-body>
+      <el-form ref="pointRef" :model="form" :rules="rules" label-width="90px">
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="管点编码" prop="pointCode">
+              <el-input v-model="form.pointCode" placeholder="请输入管点编码" maxlength="64" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="管点名称" prop="pointName">
+              <el-input v-model="form.pointName" placeholder="请输入管点名称" maxlength="128" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="所属管线" prop="networkId">
+              <el-select v-model="form.networkId" placeholder="请选择所属管线" clearable filterable style="width: 100%"
+                @change="onNetworkChange">
+                <el-option v-for="item in networkOptions" :key="item.networkId"
+                  :label="item.networkName || item.networkCode" :value="item.networkId" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="管点类型" prop="pointType">
+              <el-select v-model="form.pointType" placeholder="请选择或输入" clearable filterable allow-create
+                style="width: 100%">
+                <el-option v-for="item in pointTypeOptions" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="材质" prop="material">
+              <el-input v-model="form.material" placeholder="如 PE、钢管" maxlength="32" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="管径(mm)" prop="diameter">
+              <el-input-number v-model="form.diameter" :min="0" :precision="0" controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="埋深(m)" prop="depth">
+              <el-input-number v-model="form.depth" :min="0" :precision="2" controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="状态" prop="status">
+              <el-select v-model="form.status" placeholder="请选择状态" clearable style="width: 100%">
+                <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="经度" prop="longitude">
+              <el-input-number v-model="form.longitude" :precision="6" :step="0.0001" controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="纬度" prop="latitude">
+              <el-input-number v-model="form.latitude" :precision="6" :step="0.0001" controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="建设日期" prop="buildDate">
+              <el-date-picker v-model="form.buildDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择日期"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="位置描述" prop="address">
+              <el-input v-model="form.address" placeholder="请输入位置描述" maxlength="255" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="备注" prop="remark">
+              <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" maxlength="500"
+                show-word-limit />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" :loading="submitting" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 详情抽屉 -->
+    <el-drawer v-model="detailVisible" title="燃气管点详情" size="620px" direction="rtl">
+      <el-descriptions :column="2" border class="detail-desc">
+        <el-descriptions-item label="管点编码">{{ blankOf(detailRow.pointCode) }}</el-descriptions-item>
+        <el-descriptions-item label="管点名称">{{ blankOf(detailRow.pointName) }}</el-descriptions-item>
+        <el-descriptions-item label="所属管线">{{ networkNameOf(detailRow) }}</el-descriptions-item>
+        <el-descriptions-item label="管点类型">{{ blankOf(detailRow.pointType) }}</el-descriptions-item>
+        <el-descriptions-item label="材质">{{ blankOf(detailRow.material) }}</el-descriptions-item>
+        <el-descriptions-item label="管径">{{ numberOf(detailRow.diameter, 0) }} mm</el-descriptions-item>
+        <el-descriptions-item label="埋深">{{ numberOf(detailRow.depth, 2) }} m</el-descriptions-item>
+        <el-descriptions-item label="建设日期">{{ blankOf(detailRow.buildDate) }}</el-descriptions-item>
+        <el-descriptions-item label="经度">{{ numberOf(detailRow.longitude, 6) }}</el-descriptions-item>
+        <el-descriptions-item label="纬度">{{ numberOf(detailRow.latitude, 6) }}</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag size="small" :type="statusTagType(detailRow.status)">{{ statusLabel(detailRow.status) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ blankOf(detailRow.createTime) }}</el-descriptions-item>
+        <el-descriptions-item label="位置描述" :span="2">{{ blankOf(detailRow.address) }}</el-descriptions-item>
+        <el-descriptions-item label="备注" :span="2">{{ blankOf(detailRow.remark) }}</el-descriptions-item>
+      </el-descriptions>
+
+      <div class="detail-section">
+        <div class="section-title">关联设备</div>
+        <el-table v-loading="deviceLoading" :data="deviceList" size="small" empty-text="该管点暂无关联设备">
+          <el-table-column label="设备名称" min-width="150" show-overflow-tooltip>
+            <template #default="{ row }">{{ blankOf(row.equipmentName) }}</template>
+          </el-table-column>
+          <el-table-column label="设备类型" min-width="130" show-overflow-tooltip>
+            <template #default="{ row }">{{ blankOf(row.equipmentType) }}</template>
+          </el-table-column>
+          <el-table-column label="设备编号" min-width="150" show-overflow-tooltip>
+            <template #default="{ row }">{{ blankOf(row.equipmentId) }}</template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, reactive, ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+  getGasPipePointPage,
+  addGasPipePoint,
+  updateGasPipePoint,
+  deleteGasPipePoint,
+  getPipeNetworkOptions,
+  getPipePointDevices
+} from '@/api/pipeNetwork/basic'
+
+defineOptions({ name: 'GasPipePoint' })
+
+// ==================== 常量 ====================
+/** 管点类型:无独立字典,给常见取值并允许自由输入 */
+const pointTypeOptions = ['燃气监测', '阀门', '三通', '弯头', '调压箱', '计量表']
+
+/** 管点状态:0 正常 / 1 停用,历史数据可能存 ACTIVE 等文本,展示时原样兜底 */
+const statusOptions = [
+  { value: '0', label: '正常' },
+  { value: '1', label: '停用' }
+]
+
+// ==================== 查询 ====================
+const loading = ref(false)
+const showSearch = ref(true)
+const pointList = ref([])
+const total = ref(0)
+const queryRef = ref(null)
+const networkOptions = ref([])
+const selectedIds = ref([])
+const single = ref(true)
+const multiple = ref(true)
+
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  pointCode: '',
+  pointName: '',
+  networkId: '',
+  pointType: '',
+  status: ''
+})
+
+function getList() {
+  loading.value = true
+  getGasPipePointPage(queryParams.pageNum, queryParams.pageSize, {
+    pointCode: queryParams.pointCode,
+    pointName: queryParams.pointName,
+    networkId: queryParams.networkId,
+    pointType: queryParams.pointType,
+    status: queryParams.status
+  })
+    .then(res => {
+      const data = res.data || {}
+      pointList.value = data.records || []
+      total.value = Number(data.total || 0)
+    })
+    .catch(() => {
+      pointList.value = []
+      total.value = 0
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+
+function handleQuery() {
+  queryParams.pageNum = 1
+  getList()
+}
+
+function resetQuery() {
+  queryRef.value?.resetFields()
+  queryParams.pageNum = 1
+  queryParams.pageSize = 10
+  queryParams.pointCode = ''
+  queryParams.pointName = ''
+  queryParams.networkId = ''
+  queryParams.pointType = ''
+  queryParams.status = ''
+  getList()
+}
+
+function handleSelectionChange(selection) {
+  selectedIds.value = selection.map(item => item.pointId)
+  single.value = selection.length !== 1
+  multiple.value = selection.length === 0
+}
+
+// ==================== 新增 / 修改 ====================
+const open = ref(false)
+const title = ref('')
+const submitting = ref(false)
+const pointRef = ref(null)
+
+const defaultForm = () => ({
+  pointId: null,
+  pointCode: '',
+  pointName: '',
+  networkId: '',
+  networkName: '',
+  pointType: '',
+  material: '',
+  diameter: null,
+  depth: null,
+  longitude: null,
+  latitude: null,
+  buildDate: '',
+  status: '0',
+  address: '',
+  remark: ''
+})
+
+const form = ref(defaultForm())
+
+const rules = {
+  pointCode: [{ required: true, message: '管点编码不能为空', trigger: 'blur' }],
+  pointName: [{ required: true, message: '管点名称不能为空', trigger: 'blur' }]
+}
+
+function reset() {
+  form.value = defaultForm()
+  pointRef.value?.resetFields()
+}
+
+function cancel() {
+  open.value = false
+  reset()
+}
+
+function handleAdd() {
+  reset()
+  title.value = '新增燃气管点'
+  open.value = true
+}
+
+function handleUpdate(row) {
+  reset()
+  const target = row || pointList.value.find(item => item.pointId === selectedIds.value[0])
+  if (!target) {
+    ElMessage.warning('请选择一条记录')
+    return
+  }
+  // 只回填表单字段,避免把 createTime / delFlag 等审计列一起提交回去
+  const next = defaultForm()
+  Object.keys(next).forEach(key => {
+    if (target[key] !== undefined && target[key] !== null) {
+      next[key] = target[key]
+    }
+  })
+  form.value = next
+  title.value = '修改燃气管点'
+  open.value = true
+}
+
+function onNetworkChange(networkId) {
+  const matched = networkOptions.value.find(item => item.networkId === networkId)
+  form.value.networkName = matched ? (matched.networkName || matched.networkCode || '') : ''
+}
+
+function submitForm() {
+  pointRef.value?.validate(valid => {
+    if (!valid) return
+    submitting.value = true
+    const payload = { ...form.value }
+    const request = payload.pointId ? updateGasPipePoint(payload) : addGasPipePoint(payload)
+    request
+      .then(() => {
+        ElMessage.success(payload.pointId ? '修改成功' : '新增成功')
+        open.value = false
+        reset()
+        getList()
+      })
+      .finally(() => {
+        submitting.value = false
+      })
+  })
+}
+
+// ==================== 删除 ====================
+function handleDelete(row) {
+  const ids = row ? [row.pointId] : selectedIds.value
+  if (!ids.length) {
+    ElMessage.warning('请选择要删除的记录')
+    return
+  }
+  ElMessageBox.confirm(`确认删除选中的 ${ids.length} 条管点记录吗?`, '提示', {
+    type: 'warning',
+    confirmButtonText: '确定',
+    cancelButtonText: '取消'
+  })
+    .then(() => {
+      Promise.all(ids.map(id => deleteGasPipePoint(id)))
+        .then(() => {
+          ElMessage.success('删除成功')
+          getList()
+        })
+        .catch(() => {})
+    })
+    .catch(() => {})
+}
+
+// ==================== 详情 ====================
+const detailVisible = ref(false)
+const detailRow = ref({})
+const deviceList = ref([])
+const deviceLoading = ref(false)
+
+function handleView(row) {
+  detailRow.value = { ...row }
+  detailVisible.value = true
+  deviceList.value = []
+  if (!row.pointId) return
+  deviceLoading.value = true
+  getPipePointDevices(row.pointId)
+    .then(res => {
+      deviceList.value = res.data || []
+    })
+    .catch(() => {
+      deviceList.value = []
+    })
+    .finally(() => {
+      deviceLoading.value = false
+    })
+}
+
+// ==================== 展示工具 ====================
+function blankOf(value) {
+  return value === null || value === undefined || value === '' ? '—' : value
+}
+
+function numberOf(value, precision) {
+  if (value === null || value === undefined || value === '') return '—'
+  const num = Number(value)
+  return Number.isNaN(num) ? '—' : num.toFixed(precision)
+}
+
+function hasCoordinate(row) {
+  return row && row.longitude != null && row.latitude != null && row.longitude !== '' && row.latitude !== ''
+}
+
+function coordinateOf(row) {
+  return `${numberOf(row.longitude, 6)}, ${numberOf(row.latitude, 6)}`
+}
+
+function networkNameOf(row) {
+  if (!row) return '—'
+  if (row.networkName) return row.networkName
+  const matched = networkOptions.value.find(item => item.networkId === row.networkId)
+  return matched ? (matched.networkName || matched.networkCode || '—') : blankOf(row.networkId)
+}
+
+function statusLabel(value) {
+  if (value === null || value === undefined || value === '') return '未设置'
+  const matched = statusOptions.find(item => item.value === String(value))
+  return matched ? matched.label : String(value)
+}
+
+function statusTagType(value) {
+  const text = String(value == null ? '' : value)
+  if (text === '0' || text.toUpperCase() === 'ACTIVE') return 'success'
+  if (text === '1') return 'info'
+  return 'warning'
+}
+
+// ==================== 初始化 ====================
+function loadNetworkOptions() {
+  getPipeNetworkOptions()
+    .then(res => {
+      networkOptions.value = res.data || []
+    })
+    .catch(() => {
+      networkOptions.value = []
+    })
+}
+
+onMounted(() => {
+  loadNetworkOptions()
+  getList()
+})
+</script>
+
+<style scoped>
+.text-muted {
+  color: #909399;
+}
+
+.detail-desc {
+  margin-bottom: 16px;
+}
+
+.detail-section {
+  margin-top: 8px;
+}
+
+.section-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+  padding-left: 8px;
+  border-left: 3px solid #409eff;
+  margin-bottom: 10px;
+}
+
+.dialog-footer {
+  text-align: right;
+}
+</style>

+ 1 - 1
src/views/subSystem/gas/gasBasicadata/gasPipelintNet.vue

@@ -12,7 +12,7 @@
       </div>
       <div class="header-right">
         <el-input v-model="searchKeyword" placeholder="搜索管线名称/编号" clearable :prefix-icon="Search" style="width: 240px" />
-        <el-button type="primary" :icon="Refresh" :loading="loading" @click="loadPipelines">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" :loading="loading" @click="loadPipelines">刷新</el-button>
         <el-button :icon="FullScreen" @click="fitBounds">全图</el-button>
       </div>
     </div>

+ 1 - 1
src/views/subSystem/gas/gasBasicadata/gasTestingeq.vue

@@ -22,7 +22,7 @@
             style="width: 240px"
             size="default"
         />
-        <el-button type="primary" :icon="Refresh" @click="refreshMap">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" @click="refreshMap">刷新</el-button>
         <el-button :icon="FullScreen" @click="fitBounds">全图</el-button>
       </div>
     </div>

+ 1 - 1
src/views/subSystem/gas/gasOperamon/czspjk.vue

@@ -20,7 +20,7 @@
             :prefix-icon="Search"
             style="width: 200px"
         />
-        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" @click="refreshData">刷新</el-button>
         <el-button :icon="FullScreen" @click="toggleFullscreen">全屏</el-button>
       </div>
     </div>

+ 16 - 9
src/views/subSystem/gas/gasOperamon/gxlljc.vue

@@ -15,7 +15,7 @@
       </div>
       <div class="header-right">
         <el-input v-model="searchKeyword" placeholder="搜索测点名称/管线" clearable :prefix-icon="Search" style="width:200px" />
-        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" @click="refreshData">刷新</el-button>
       </div>
     </div>
 
@@ -129,7 +129,7 @@
               <span>历史监测记录</span>
 
             </div>
-            <el-table :data="pagedHistory" border stripe size="small">
+            <el-table :data="pagedHistory" size="small">
               <el-table-column prop="time" label="采集时间" width="160" />
               <el-table-column prop="flow" label="瞬时流量(Nm³/h)" width="140">
                 <template #default="{ row }">
@@ -151,13 +151,15 @@
               <el-table-column prop="collector" label="采集设备" min-width="140" />
               <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
             </el-table>
-            <el-pagination
-                v-model:current-page="historyPage"
-                v-model:page-size="historyPageSize"
-                :total="historyData.length"
-                layout="total, prev, pager, next"
-                small
-            />
+            <div class="history-pagination">
+              <pagination
+                  v-model:page="historyPage"
+                  v-model:limit="historyPageSize"
+                  :total="historyData.length"
+                  :auto-scroll="false"
+                  layout="total, prev, pager, next"
+              />
+            </div>
           </div>
         </template>
 
@@ -619,4 +621,9 @@ onBeforeUnmount(() => {
   margin-bottom: 12px;
   font-weight: 600;
 }
+/* 历史记录分页:沿用若依分页组件,去掉其自带的底色与上下留白 */
+.history-pagination :deep(.pagination-container) {
+  padding: 12px 0 0;
+  background: transparent;
+}
 </style>

+ 17 - 9
src/views/subSystem/gas/gasOperamon/gxyljc.vue

@@ -15,7 +15,7 @@
       </div>
       <div class="header-right">
         <el-input v-model="searchKeyword" placeholder="搜索测点名称/管线" clearable :prefix-icon="Search" style="width:200px" />
-        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" @click="refreshData">刷新</el-button>
       </div>
     </div>
 
@@ -126,7 +126,7 @@
             <div class="table-header">
               <span>历史监测记录</span>
             </div>
-            <el-table :data="pagedHistory" border stripe size="small">
+            <el-table :data="pagedHistory" size="small">
               <el-table-column prop="time" label="采集时间" width="160" />
               <el-table-column prop="pressure" label="压力值(kPa)" width="120">
                 <template #default="{ row }">
@@ -145,13 +145,16 @@
               <el-table-column prop="collector" label="采集设备" min-width="140" />
               <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
             </el-table>
-            <el-pagination
-                v-model:current-page="historyPage"
-                v-model:page-size="historyPageSize"
-                :total="pagedTotal"
-                layout="total, prev, pager, next"
-                small
-            />
+            <div class="history-pagination">
+              <pagination
+                  v-model:page="historyPage"
+                  v-model:limit="historyPageSize"
+                  :total="pagedTotal"
+                  :auto-scroll="false"
+                  layout="total, prev, pager, next"
+                  @pagination="loadHistory"
+              />
+            </div>
           </div>
 
         </template>
@@ -539,4 +542,9 @@ onBeforeUnmount(() => {
   margin-bottom: 12px;
   font-weight: 600;
 }
+/* 历史记录分页:沿用若依分页组件,去掉其自带的底色与上下留白 */
+.history-pagination :deep(.pagination-container) {
+  padding: 12px 0 0;
+  background: transparent;
+}
 </style>

+ 1 - 1
src/views/subSystem/gas/gasOperamon/ssjcyzt.vue

@@ -15,7 +15,7 @@
       </div>
       <div class="header-right">
         <el-input v-model="searchKeyword" placeholder="搜索设施/地址" clearable :prefix-icon="Search" style="width:220px" @input="onSearch" />
-        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" @click="refreshData">刷新</el-button>
         <el-button :icon="FullScreen" @click="fitBounds">全图</el-button>
       </div>
     </div>

+ 2 - 2
src/views/subSystem/gas/gasOperamon/xldxkjkrqtndjc.vue

@@ -14,7 +14,7 @@
       </div>
       <div class="header-right">
         <el-input v-model="searchKeyword" placeholder="搜索点位/地址" clearable :prefix-icon="Search" style="width:200px" />
-        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" @click="refreshData">刷新</el-button>
         <el-button :icon="Download" @click="exportReport">导出报表</el-button>
       </div>
     </div>
@@ -81,7 +81,7 @@
           <el-empty v-if="filteredPoints.length === 0" description="暂无匹配点位" :image-size="50" />
         </div>
         <div class="pagination-area">
-          <el-pagination small layout="prev, pager, next" :total="filteredPoints.length" :page-size="pageSize" v-model:current-page="currentPage" />
+          <el-pagination small background layout="prev, pager, next" :total="filteredPoints.length" :page-size="pageSize" v-model:current-page="currentPage" />
         </div>
       </div>
 

+ 1 - 1
src/views/subSystem/gas/gasfxpg/gwfxpg.vue

@@ -20,7 +20,7 @@
           :prefix-icon="Search"
           style="width: 220px"
         />
-        <el-button type="primary" :icon="Location" @click="locateSearch">定位</el-button>
+        <el-button type="primary" plain :icon="Location" @click="locateSearch">定位</el-button>
         <el-button :icon="Refresh" @click="refreshMap">刷新</el-button>
         <el-button :icon="FullScreen" @click="fitBounds">全图</el-button>
       </div>

+ 2 - 2
src/views/subSystem/gas/gasfxpg/gwfxpgqd.vue

@@ -20,7 +20,7 @@
             :prefix-icon="Search"
             style="width: 220px"
         />
-        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" @click="refreshData">刷新</el-button>
         <el-button :icon="Download" @click="exportReport">导出报表</el-button>
       </div>
     </div>
@@ -94,7 +94,7 @@
         <div class="pagination-area">
           <el-pagination
               small
-              layout="prev, pager, next"
+              background layout="prev, pager, next"
               :total="filteredPipelines.length"
               :page-size="pageSize"
               v-model:current-page="currentPage"

+ 2 - 2
src/views/subSystem/gas/gasfxpg/gwfxxq.vue

@@ -20,7 +20,7 @@
             :prefix-icon="Search"
             style="width: 220px"
         />
-        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" @click="refreshData">刷新</el-button>
         <el-button :icon="Download" @click="exportReport">导出报告</el-button>
       </div>
     </div>
@@ -63,7 +63,7 @@
           </div>
         </div>
         <div class="pagination-area">
-          <el-pagination small layout="prev, pager, next" :total="filteredPipelines.length" :page-size="pageSize" v-model:current-page="currentPage" />
+          <el-pagination small background layout="prev, pager, next" :total="filteredPipelines.length" :page-size="pageSize" v-model:current-page="currentPage" />
         </div>
       </div>
 

+ 1 - 1
src/views/subSystem/gas/gasfxpg/gxfxpgsst.vue

@@ -21,7 +21,7 @@
           :prefix-icon="Search"
           style="width: 200px"
         />
-        <el-button type="primary" :icon="Location" @click="locateSearch">定位</el-button>
+        <el-button type="primary" plain :icon="Location" @click="locateSearch">定位</el-button>
         <el-button :icon="Refresh" @click="refreshMap">刷新</el-button>
         <el-button :icon="FullScreen" @click="fitBounds">全图</el-button>
       </div>

+ 2 - 2
src/views/subSystem/gas/gashiddendanger/gasApicture.vue

@@ -26,7 +26,7 @@
           <el-option label="较大隐患" value="major" />
           <el-option label="一般隐患" value="general" />
         </el-select>
-        <el-button type="primary" :icon="Refresh" @click="refreshMap">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" @click="refreshMap">刷新</el-button>
         <el-button :icon="FullScreen" @click="fitBounds">全图</el-button>
       </div>
     </div>
@@ -73,7 +73,7 @@
         <div class="pagination-area">
           <el-pagination
               small
-              layout="prev, pager, next"
+              background layout="prev, pager, next"
               :total="filteredHazards.length"
               :page-size="pageSize"
               v-model:current-page="currentPage"

+ 17 - 10
src/views/subSystem/gas/gasjcyj/gxllyctx.vue

@@ -22,7 +22,7 @@
             style="width: 260px"
             size="default"
         />
-        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" @click="refreshData">刷新</el-button>
         <el-button :icon="Download" @click="exportReport">导出报表</el-button>
       </div>
     </div>
@@ -88,7 +88,7 @@
           </div>
         </div>
         <div class="pagination-area" v-if="realtimeAlerts.length > pageSize">
-          <el-pagination small layout="prev, pager, next" :total="realtimeAlerts.length" :page-size="pageSize" v-model:current-page="realtimePage" />
+          <el-pagination small background layout="prev, pager, next" :total="realtimeAlerts.length" :page-size="pageSize" v-model:current-page="realtimePage" />
         </div>
       </div>
 
@@ -106,7 +106,7 @@
           />
         </div>
         <div class="alert-list history">
-          <el-table :data="paginatedHistory" stripe border style="width: 100%" @row-click="viewAlertDetail">
+          <el-table :data="paginatedHistory" style="width: 100%" @row-click="viewAlertDetail">
             <el-table-column prop="pointName" label="监测点位" min-width="140" show-overflow-tooltip />
             <el-table-column prop="address" label="位置" min-width="160" show-overflow-tooltip />
             <el-table-column prop="flow" label="瞬时流量(Nm³/h)" width="130" sortable>
@@ -131,13 +131,15 @@
           </el-table>
         </div>
         <div class="pagination-area">
-          <el-pagination
-              small
-              layout="total, prev, pager, next"
-              :total="filteredHistory.length"
-              :page-size="historyPageSize"
-              v-model:current-page="historyPage"
-          />
+          <div class="history-pagination">
+            <pagination
+                v-model:page="historyPage"
+                :limit="historyPageSize"
+                :total="filteredHistory.length"
+                :auto-scroll="false"
+                layout="total, prev, pager, next"
+            />
+          </div>
         </div>
       </div>
     </div>
@@ -461,4 +463,9 @@ onBeforeUnmount(() => {
 .section-title { font-weight: 600; margin: 16px 0 12px 0; }
 .trend-chart { height: 200px; }
 .action-section { margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end; }
+/* 历史记录分页:沿用若依分页组件,去掉其自带的底色与上下留白 */
+.history-pagination :deep(.pagination-container) {
+  padding: 12px 0 0;
+  background: transparent;
+}
 </style>

+ 17 - 15
src/views/subSystem/gas/gasjcyj/gxylyctx.vue

@@ -22,7 +22,7 @@
             style="width: 260px"
             size="default"
         />
-        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" @click="refreshData">刷新</el-button>
         <el-button :icon="Download" @click="exportReport">导出报表</el-button>
       </div>
     </div>
@@ -91,7 +91,7 @@
         <div class="pagination-area" v-if="realtimeEquipments.length > realtimePageSize">
           <el-pagination
             small
-            layout="prev, pager, next"
+            background layout="prev, pager, next"
             :total="realtimeEquipments.length"
             :page-size="realtimePageSize"
             v-model:current-page="realtimePage"
@@ -114,7 +114,7 @@
           />
         </div>
         <div class="alert-list history">
-          <el-table :data="paginatedHistory" stripe border style="width: 100%" @row-click="viewAlertDetail">
+          <el-table :data="paginatedHistory" style="width: 100%" @row-click="viewAlertDetail">
             <el-table-column prop="pointName" label="监测点位" min-width="140" show-overflow-tooltip />
             <el-table-column prop="address" label="位置" min-width="160" show-overflow-tooltip />
             <el-table-column prop="pressure" label="异常压力(kPa)" width="110" sortable>
@@ -140,14 +140,15 @@
           </el-table>
         </div>
         <div class="pagination-area">
-          <el-pagination
-              small
-              layout="total, prev, pager, next"
-              :total="filteredHistory.length"
-              :page-size="historyPageSize"
-              v-model:current-page="historyPage"
-              @update:current-page="handleHistoryPageChange"
-          />
+          <div class="history-pagination">
+            <pagination
+                v-model:page="historyPage"
+                :limit="historyPageSize"
+                :total="filteredHistory.length"
+                :auto-scroll="false"
+                layout="total, prev, pager, next"
+            />
+          </div>
         </div>
       </div>
     </div>
@@ -257,10 +258,6 @@ function handleRealtimePageChange(newPage) {
   realtimePage.value = newPage
 }
 
-function handleHistoryPageChange(newPage) {
-  historyPage.value = newPage
-  loadHistory()
-}
 function selectEquipment(eq) {
   selectedEquipment.value = eq
   currentDeviceCode.value = eq.equipmentCode
@@ -470,4 +467,9 @@ onBeforeUnmount(() => { try { chartInstance?.dispose() } catch {}; chartInstance
 .section-title { font-weight: 600; margin: 16px 0 12px 0; }
 .trend-chart { height: 200px; }
 .action-section { margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end; }
+/* 历史记录分页:沿用若依分页组件,去掉其自带的底色与上下留白 */
+.history-pagination :deep(.pagination-container) {
+  padding: 12px 0 0;
+  background: transparent;
+}
 </style>

+ 17 - 10
src/views/subSystem/gas/gasjcyj/krqindbj.vue

@@ -25,7 +25,7 @@
         <el-select v-model="companyFilter" placeholder="燃气公司" clearable style="width: 140px" size="default">
           <el-option label="沅陵燃气" value="yuanling" />
         </el-select>
-        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" @click="refreshData">刷新</el-button>
         <el-button :icon="Download" @click="exportReport">导出报表</el-button>
       </div>
     </div>
@@ -109,7 +109,7 @@
           />
         </div>
         <div class="alert-list history">
-          <el-table :data="paginatedHistory" stripe border style="width: 100%" @row-click="viewAlertDetail">
+          <el-table :data="paginatedHistory" style="width: 100%" @row-click="viewAlertDetail">
             <el-table-column prop="stationName" label="场站名称" min-width="140" show-overflow-tooltip />
             <el-table-column prop="companyName" label="燃气公司" width="100" />
             <el-table-column prop="address" label="位置" min-width="150" show-overflow-tooltip />
@@ -140,14 +140,16 @@
           </el-table>
         </div>
         <div class="pagination-area">
-          <el-pagination
-              small
-              layout="total, prev, pager, next"
-              :total="historyTotal"
-              :page-size="historyPageSize"
-              v-model:current-page="historyPage"
-              @current-change="loadHistory"
-          />
+          <div class="history-pagination">
+            <pagination
+                v-model:page="historyPage"
+                :limit="historyPageSize"
+                :total="historyTotal"
+                :auto-scroll="false"
+                layout="total, prev, pager, next"
+                @pagination="loadHistory"
+            />
+          </div>
         </div>
       </div>
     </div>
@@ -498,4 +500,9 @@ onBeforeUnmount(() => {
 .section-title { font-weight: 600; margin: 16px 0 12px 0; }
 .trend-chart { height: 200px; }
 .action-section { margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end; }
+/* 历史记录分页:沿用若依分页组件,去掉其自带的底色与上下留白 */
+.history-pagination :deep(.pagination-container) {
+  padding: 12px 0 0;
+  background: transparent;
+}
 </style>

+ 1 - 1
src/views/subSystem/gas/gasjcyj/rqjcbjyzt.vue

@@ -14,7 +14,7 @@
       </div>
       <div class="header-right">
         <el-input v-model="searchKeyword" placeholder="搜索设施/地址" clearable :prefix-icon="Search" style="width:200px" @input="onSearch" />
-        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" @click="refreshData">刷新</el-button>
         <el-button :icon="FullScreen" @click="fitBounds">全图</el-button>
       </div>
     </div>

+ 2 - 2
src/views/subSystem/gas/gasjcyj/xldxkjkrqindbj.vue

@@ -21,7 +21,7 @@
             style="width: 200px"
             size="default"
         />
-        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
+        <el-button type="primary" plain :icon="Refresh" @click="refreshData">刷新</el-button>
         <el-button :icon="Download" @click="exportReport">导出报表</el-button>
       </div>
     </div>
@@ -94,7 +94,7 @@
           </div>
         </div>
         <div class="pagination-area" v-if="filteredAlerts.length > pageSize">
-          <el-pagination small layout="prev, pager, next" :total="filteredAlerts.length" :page-size="pageSize" v-model:current-page="currentPage" />
+          <el-pagination small background layout="prev, pager, next" :total="filteredAlerts.length" :page-size="pageSize" v-model:current-page="currentPage" />
         </div>
       </div>
 

+ 210 - 132
src/views/subSystem/gas/ghome/gHome.vue

@@ -2,8 +2,8 @@
 import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
 import * as echarts from 'echarts'
 import { Odometer, CircleCheckFilled, VideoCameraFilled, WarnTriangleFilled } from '@element-plus/icons-vue'
-import { convertPipeline, convertPoints } from '@/utils/coordTransform'
-import { getGasDashboardSummary } from '@/api/pipeNetwork/basic'
+import { wgs84ToBd09 } from '@/utils/coordTransform'
+import { getGasDashboardSummary, getGisPipeLines } from '@/api/pipeNetwork/basic'
 
 const nowText = ref('2025/12/31 12:59:59')
 const weekText = ref('星期三')
@@ -16,10 +16,10 @@ const dashboardData = ref({})
 const baseCards = computed(() => {
   const bs = dashboardData.value.baseStats || {}
   return [
-    { label: '监测点总数', value: bs.totalPoints || 0, unit: '个', icon: Odometer, color: '#60b8ff' },
-    { label: '在线监测点', value: bs.onlinePoints || 0, unit: '个', icon: CircleCheckFilled, color: '#52e0a8' },
-    { label: '离线监测点', value: bs.offlinePoints || 0, unit: '个', icon: VideoCameraFilled, color: '#ff8e8e' },
-    { label: '异常设备', value: bs.alarmCount || 0, unit: '个', icon: WarnTriangleFilled, color: '#ffb54d' }
+    { label: '燃气设备', value: bs.totalEquipments || 0, unit: '台', icon: Odometer, color: '#60b8ff' },
+    { label: '在线设备', value: bs.onlineEquipments || 0, unit: '台', icon: CircleCheckFilled, color: '#52e0a8' },
+    { label: '离线设备', value: bs.offlineEquipments || 0, unit: '台', icon: VideoCameraFilled, color: '#ff8e8e' },
+    { label: '异常设备', value: bs.alarmCount || 0, unit: '台', icon: WarnTriangleFilled, color: '#ffb54d' }
   ]
 })
 
@@ -27,8 +27,7 @@ const operationSummary = computed(() => {
   const ps = dashboardData.value.pipeStats || {}
   return [
     { label: '管网总长度', value: (ps.totalLength || 0) + 'km' },
-    { label: '燃气管网', value: (ps.gasNetworks || 0) + '条' },
-    { label: '管网总数', value: (ps.totalNetworks || 0) + '条' }
+    { label: '燃气管网', value: (ps.gasNetworks || 0) + '条' }
   ]
 })
 
@@ -68,70 +67,22 @@ const hazardSummary = computed(() => {
 const mapKpis = computed(() => {
   const bs = dashboardData.value.baseStats || {}
   const es = dashboardData.value.equipmentStats || {}
-  const total = bs.totalPoints || 0
-  const online = bs.onlinePoints || 0
+  const onlineRate = bs.onlineRate
+  const avgPressure = es.avgPressure
   return [
-    { label: '在线率', value: total > 0 ? (online / total * 100).toFixed(1) + '%' : '--' },
-    { label: '压力均值', value: (es.avgPressure || 0) + 'MPa' },
+    { label: '在线率', value: onlineRate != null ? Number(onlineRate).toFixed(1) + '%' : '--' },
+    { label: '压力均值', value: avgPressure != null ? Number(avgPressure).toFixed(2) + 'MPa' : '--' },
     { label: '今日告警', value: (es.todayAlarm || 0) + '条' }
   ]
 })
 
-const pipelineLines = [
-  {
-    name: '燃气1',
-    color: '#ff3030',
-    points: [
-      [110.404947, 28.433419],
-      [110.404836, 28.433429],
-      [110.403275, 28.433571],
-      [110.401084, 28.433876],
-      [110.399601, 28.433876],
-      [110.398112, 28.433979],
-      [110.396717, 28.434314],
-      [110.394299, 28.434538],
-      [110.394352, 28.434887],
-      [110.391969, 28.434985],
-      [110.384374, 28.435695],
-      [110.381418, 28.43559],
-      [110.377993, 28.436193],
-      [110.376062, 28.436385],
-      [110.374231, 28.43655],
-      [110.372584, 28.437107],
-      [110.371292, 28.4377],
-      [110.370581, 28.438198],
-      [110.369831, 28.438821]
-    ]
-  },
-  {
-    name: '燃气2',
-    color: '#ff3030',
-points: [
-      [110.361433, 28.454979],
-      [110.362363, 28.453169],
-      [110.362851, 28.452213],
-      [110.364377, 28.452443],
-      [110.365138, 28.45246],
-      [110.366462, 28.452157],
-      [110.369549, 28.451441],
-      [110.37219, 28.450897],
-      [110.372753, 28.450901],
-      [110.373875, 28.451194],
-      [110.373913, 28.451107],
-      [110.374597, 28.451476]
-    ]
-  }
-]
-
-const pipePoints = [
-  { name: 'A点', value: '0.22MPa', lng: 110.362363, lat: 28.453169 },
-  { name: 'B点', value: '0.24MPa', lng: 110.366462, lat: 28.452157 },
-  { name: 'C点', value: '0.18MPa', lng: 110.373875, lat: 28.451194},
-  { name: 'D点', value: '0.23MPa', lng: 110.401084, lat: 28.433876 },
-  { name: 'E点', value: '0.25MPa', lng: 110.391969, lat: 28.434985 },
-  { name: 'F点', value: '0.19MPa', lng: 110.376062, lat: 28.436385 },
-  { name: 'G点', value: '0.21MPa', lng: 110.370581, lat: 28.438198}
-]
+/** 压力最新采集时间,用于说明均值取自哪个时刻 */
+const pressureTimeText = computed(() => {
+  const text = dashboardData.value.pressureUpdatedAt
+  if (!text) return '--'
+  return text.length > 8 ? text.slice(-8) : text
+})
+
 
 const mapWrapRef = ref(null)
 const repairChartRef = ref(null)
@@ -157,8 +108,10 @@ function updateClock() {
 
 function initRepairChart() {
   if (!repairChartRef.value) return
-  repairChart?.dispose()
-  repairChart = echarts.init(repairChartRef.value)
+  // 定时刷新时复用已有实例,避免整块图表重建闪烁
+  if (!repairChart) {
+    repairChart = echarts.init(repairChartRef.value)
+  }
   const data = repairRecords.value
   repairChart.setOption({
     grid: { left: 30, right: 12, top: 20, bottom: 24, containLabel: true },
@@ -192,8 +145,9 @@ function initRepairChart() {
 
 function initHazardChart() {
   if (!hazardChartRef.value) return
-  hazardChart?.dispose()
-  hazardChart = echarts.init(hazardChartRef.value)
+  if (!hazardChart) {
+    hazardChart = echarts.init(hazardChartRef.value)
+  }
   const data = hazardSummary.value
   hazardChart.setOption({
     grid: { left: 18, right: 18, top: 10, bottom: 10, containLabel: true },
@@ -245,42 +199,132 @@ function waitForBMapGL() {
   })
 }
 
-function addPipeLine(BMapGL, pipe) {
-  const bmapPoints = pipe.points.map(([lng, lat]) => new BMapGL.Point(lng, lat))
-  const polyline = new BMapGL.Polyline(bmapPoints, {
-    strokeColor: pipe.color,
-    strokeWeight: 4,
-    strokeOpacity: 0.95
+/** 管网状态 → 线条颜色,取值见字典 pipe_network_status:0 正常 / 1 维护中 / 2 异常 */
+const PIPE_STATUS_COLOR = { 0: '#ff4d4f', 1: '#ffb020', 2: '#ff1f1f' }
+
+/** 压力点状态 → 标签配色 */
+const PRESSURE_STATUS_STYLE = {
+  normal: { border: 'rgba(82, 224, 168, 0.75)', text: '#d8ffe9' },
+  warning: { border: 'rgba(255, 181, 77, 0.85)', text: '#ffe9c7' },
+  danger: { border: 'rgba(255, 48, 48, 0.9)', text: '#ffd6d6' },
+  offline: { border: 'rgba(150, 170, 190, 0.7)', text: '#cfd8e3' }
+}
+
+/** 地图叠加物,刷新时整体清除后重画 */
+let pipeOverlays = []
+let pressureOverlays = []
+
+function clearOverlays(overlays) {
+  if (mapInstance) {
+    overlays.forEach(overlay => mapInstance.removeOverlay(overlay))
+  }
+  return []
+}
+
+/**
+ * 绘制燃气管段
+ *
+ * 燃气管网每条记录只有起终点坐标,按状态着色,不再逐点画装饰圆(数百条管段会拖慢地图)。
+ */
+function drawPipelines(BMapGL, features) {
+  pipeOverlays = clearOverlays(pipeOverlays)
+  features.forEach(feature => {
+    const props = feature.properties || {}
+    const coordinates = feature.geometry && feature.geometry.coordinates
+    if (!Array.isArray(coordinates) || coordinates.length < 2) return
+    const path = coordinates
+      .map(([lng, lat]) => {
+        const [bdLng, bdLat] = wgs84ToBd09(Number(lng), Number(lat))
+        return new BMapGL.Point(bdLng, bdLat)
+      })
+      .filter(point => !Number.isNaN(point.lng) && !Number.isNaN(point.lat))
+    if (path.length < 2) return
+
+    const status = String(props.status == null ? '' : props.status)
+    const line = new BMapGL.Polyline(path, {
+      strokeColor: PIPE_STATUS_COLOR[status] || PIPE_STATUS_COLOR[0],
+      strokeWeight: status === '2' ? 5 : 4,
+      strokeOpacity: 0.92
+    })
+    mapInstance.addOverlay(line)
+    pipeOverlays.push(line)
   })
-  mapInstance.addOverlay(polyline)
-
-  bmapPoints.forEach((point, index) => {
-    const ring = new BMapGL.Circle(point, 14, {
-      strokeColor: '#ff3030',
-      strokeWeight: 2,
-      strokeOpacity: 0.7,
-      fillColor: 'rgba(255,48,48,0.08)',
-      fillOpacity: 0.18
+}
+
+/**
+ * 绘制压力监测点,标签显示站点名称与最新压力值
+ */
+function drawPressurePoints(BMapGL, points) {
+  pressureOverlays = clearOverlays(pressureOverlays)
+  points.forEach(point => {
+    const lng = Number(point.lng)
+    const lat = Number(point.lat)
+    if (!lng || !lat) return
+    const [bdLng, bdLat] = wgs84ToBd09(lng, lat)
+    const position = new BMapGL.Point(bdLng, bdLat)
+
+    const marker = new BMapGL.Marker(position)
+    mapInstance.addOverlay(marker)
+    pressureOverlays.push(marker)
+
+    const style = PRESSURE_STATUS_STYLE[String(point.status || 'normal')] || PRESSURE_STATUS_STYLE.normal
+    const pressureText = point.latestPressure == null
+      ? '--'
+      : (Number(point.latestPressure) / 1000).toFixed(2) + 'MPa'
+    const label = new BMapGL.Label(`${point.name || '压力点'} ${pressureText}`, {
+      offset: new BMapGL.Size(-48, -44)
+    })
+    label.setPosition(position)
+    label.setStyle({
+      color: style.text,
+      background: 'rgba(12, 46, 82, 0.86)',
+      border: `1px solid ${style.border}`,
+      borderRadius: '8px',
+      padding: '5px 9px',
+      fontSize: '12px',
+      lineHeight: '16px',
+      whiteSpace: 'nowrap'
     })
-    mapInstance.addOverlay(ring)
+    mapInstance.addOverlay(label)
+    pressureOverlays.push(label)
   })
 }
 
-function addPipePoint(BMapGL, point) {
-  const markerPoint = new BMapGL.Point(point.lng, point.lat)
-  const marker = new BMapGL.Marker(markerPoint)
-  mapInstance.addOverlay(marker)
-  const label = new BMapGL.Label(`${point.name} ${point.value}`, { offset: new BMapGL.Size(-42, -40) })
-  label.setPosition(markerPoint)
-  label.setStyle({
-    color: '#d8faff',
-    background: 'rgba(18, 68, 112, 0.75)',
-    border: '1px solid rgba(255, 48, 48, 0.55)',
-    borderRadius: '8px',
-    padding: '5px 8px',
-    fontSize: '12px'
+/** 按管网实际范围自动缩放,只有一条管段时按固定级别定位 */
+function fitPipeView(BMapGL, features) {
+  const points = []
+  features.forEach(feature => {
+    const coordinates = feature.geometry && feature.geometry.coordinates
+    if (!Array.isArray(coordinates)) return
+    coordinates.forEach(([lng, lat]) => {
+      const [bdLng, bdLat] = wgs84ToBd09(Number(lng), Number(lat))
+      points.push(new BMapGL.Point(bdLng, bdLat))
+    })
   })
-  mapInstance.addOverlay(label)
+  if (!points.length) return
+  if (points.length === 1) {
+    mapInstance.centerAndZoom(points[0], 16)
+    return
+  }
+  mapInstance.setViewport(points)
+}
+
+/** 拉取燃气管网数据并上图 */
+async function loadPipeLines(BMapGL) {
+  try {
+    const res = await getGisPipeLines({ networkType: '0' })
+    const features = (res.data && res.data.features) || []
+    if (!features.length) {
+      mapError.value = '暂无燃气管网坐标数据,请先在管网基础数据中补齐起终点经纬度。'
+      return
+    }
+    mapError.value = ''
+    drawPipelines(BMapGL, features)
+    fitPipeView(BMapGL, features)
+  } catch (error) {
+    console.error('[燃气大屏] 加载燃气管网失败', error)
+    mapError.value = '燃气管网数据加载失败,请稍后重试。'
+  }
 }
 
 async function initMap() {
@@ -293,26 +337,43 @@ async function initMap() {
     mapInstance.enableScrollWheelZoom(true)
     mapInstance.setMapStyleV2({ styleId: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' })
 
-    const bdPipelineLines = pipelineLines.map(pipe => convertPipeline(pipe))
-    const bdPipePoints = convertPoints(pipePoints)
-    bdPipelineLines.forEach(pipe => addPipeLine(BMapGL, pipe))
-    bdPipePoints.forEach(point => addPipePoint(BMapGL, point))
+    drawPressurePoints(BMapGL, dashboardData.value.pressurePoints || [])
+    await loadPipeLines(BMapGL)
   } catch (error) {
     mapError.value = '百度地图加载失败,请检查地图脚本或网络连接。'
   }
 }
 
+/** 定时刷新压力点位,不重建底图 */
+function refreshPressureLayer() {
+  if (!mapInstance || !window.BMapGL) return
+  drawPressurePoints(window.BMapGL, dashboardData.value.pressurePoints || [])
+}
+
 function handleResize() {
   repairChart?.resize()
   hazardChart?.resize()
 }
 
-async function loadDashboardData() {
-  loading.value = true
+/** 仪表盘刷新间隔(毫秒),压力点位与在线率按此周期同步最新采集值 */
+const REFRESH_INTERVAL = 30 * 1000
+let refreshTimer = null
+
+async function loadDashboardData(silent = false) {
+  if (!silent) loading.value = true
   try {
     const res = await getGasDashboardSummary()
     dashboardData.value = res.data || {}
-  } catch (e) { console.error(e) } finally { loading.value = false }
+    refreshPressureLayer()
+    if (silent) {
+      initRepairChart()
+      initHazardChart()
+    }
+  } catch (e) {
+    console.error('[燃气大屏] 加载仪表盘数据失败', e)
+  } finally {
+    if (!silent) loading.value = false
+  }
 }
 
 onMounted(async () => {
@@ -322,15 +383,19 @@ onMounted(async () => {
   await nextTick()
   initRepairChart()
   initHazardChart()
-  initMap()
+  await initMap()
+  refreshTimer = window.setInterval(() => loadDashboardData(true), REFRESH_INTERVAL)
   window.addEventListener('resize', handleResize)
 })
 
 onBeforeUnmount(() => {
   if (clockTimer) window.clearInterval(clockTimer)
+  if (refreshTimer) window.clearInterval(refreshTimer)
   window.removeEventListener('resize', handleResize)
   repairChart?.dispose()
   hazardChart?.dispose()
+  pipeOverlays = []
+  pressureOverlays = []
   mapInstance = null
 })
 </script>
@@ -401,18 +466,15 @@ onBeforeUnmount(() => {
 
       <div class="center-panel">
         <div class="map-stage">
+          <!-- 管网、在线率、压力、告警合并为一条指标条,按份均分宽度,窄屏也不会溢出 -->
           <div class="map-stage-top">
-            <div class="summary-strip">
-              <div v-for="item in operationSummary" :key="item.label" class="summary-item">
-                <span class="summary-label">{{ item.label }}</span>
-                <strong class="summary-value">{{ item.value }}</strong>
-              </div>
+            <div v-for="item in operationSummary" :key="item.label" class="summary-item">
+              <span class="summary-label">{{ item.label }}</span>
+              <strong class="summary-value">{{ item.value }}</strong>
             </div>
-            <div class="map-kpis">
-              <div v-for="item in mapKpis" :key="item.label" class="map-kpi">
-                <span>{{ item.label }}</span>
-                <strong>{{ item.value }}</strong>
-              </div>
+            <div v-for="item in mapKpis" :key="item.label" class="map-kpi">
+              <span>{{ item.label }}</span>
+              <strong>{{ item.value }}</strong>
             </div>
           </div>
 
@@ -424,6 +486,9 @@ onBeforeUnmount(() => {
             <div class="mini-row"><span>管网总长度</span><strong>{{ (dashboardData.pipeStats && dashboardData.pipeStats.totalLength) || 0 }}km</strong></div>
             <div class="mini-row"><span>燃气管网</span><strong>{{ (dashboardData.pipeStats && dashboardData.pipeStats.gasNetworks) || 0 }}条</strong></div>
             <div class="mini-row"><span>设备总数</span><strong>{{ (dashboardData.equipmentStats && dashboardData.equipmentStats.totalEquipment) || 0 }}台</strong></div>
+            <div class="mini-row" :title="dashboardData.pressureUpdatedAt || '暂无压力采集时间'">
+              <span>压力采集</span><strong>{{ pressureTimeText }}</strong>
+            </div>
           </div>
 
           <div class="map-panel point-panel">
@@ -927,26 +992,22 @@ onBeforeUnmount(() => {
   right: 14px;
   z-index: 3;
   display: flex;
-  justify-content: space-between;
-  gap: 12px;
+  align-items: stretch;
+  gap: 8px;
   pointer-events: none;
 }
 
-.summary-strip,
-.map-kpis {
-  display: flex;
-  gap: 10px;
-}
-
 .summary-item,
 .map-kpi {
-  min-width: 110px;
-  padding: 10px 14px;
+  flex: 1 1 0;
+  min-width: 0;
+  padding: 8px 10px;
   border-radius: 10px;
   background: rgba(8, 31, 54, 0.82);
   border: 1px solid rgba(79, 177, 238, 0.18);
   backdrop-filter: blur(8px);
   transition: all 0.3s;
+  overflow: hidden;
 }
 
 .summary-label,
@@ -955,6 +1016,9 @@ onBeforeUnmount(() => {
   color: #91d7ff;
   font-size: 12px;
   letter-spacing: 0.5px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
 .summary-value,
@@ -962,9 +1026,12 @@ onBeforeUnmount(() => {
   display: block;
   margin-top: 4px;
   color: #fff;
-  font-size: 22px;
+  font-size: 20px;
   font-weight: 800;
-  letter-spacing: 1px;
+  letter-spacing: 0.5px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
 .map-view {
@@ -1291,7 +1358,18 @@ onBeforeUnmount(() => {
 
   .summary-item,
   .map-kpi {
-    min-width: 96px;
+    min-width: 0;
+    padding: 7px 8px;
+  }
+
+  .summary-label,
+  .map-kpi span {
+    font-size: 11px;
+  }
+
+  .summary-value,
+  .map-kpi strong {
+    font-size: 16px;
   }
 }
 </style>

+ 108 - 100
src/views/subSystem/hidden/HazardList.vue

@@ -1,103 +1,109 @@
 <template>
-  <div class="hazard-container">
-    <el-card class="search-card">
-      <el-form :inline="true" :model="searchForm">
-        <el-form-item label="隐患类型">
-          <el-select v-model="searchForm.hazardType" placeholder="请选择" clearable style="width:140px">
-            <el-option v-for="d in hazardTypeDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="隐患等级">
-          <el-select v-model="searchForm.hazardLevel" placeholder="请选择" clearable style="width:140px">
-            <el-option v-for="d in hazardLevelDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="状态">
-          <el-select v-model="searchForm.hazardStatus" placeholder="请选择" clearable style="width:140px">
-            <el-option v-for="d in hazardStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="报送状态">
-          <el-select v-model="searchForm.isSubmitted" placeholder="请选择" clearable style="width:140px">
-            <el-option label="未报送" :value="0" />
-            <el-option label="已报送" :value="1" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="关联管网">
-          <el-select-v2 v-model="searchForm.networkId" placeholder="请选择" clearable filterable style="width:160px" :options="networkSelectOptions" @focus="loadNetworkOptions" />
-        </el-form-item>
-        <el-form-item label="关联设备">
-          <el-select-v2 v-model="searchForm.equipmentId" placeholder="请选择" clearable filterable style="width:160px" :options="equipmentSelectOptions" @focus="loadEquipmentOptions" />
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" @click="handleSearch">查询</el-button>
-          <el-button @click="handleReset">重置</el-button>
-          <el-button type="success" @click="handleAdd">新增</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
+  <div class="app-container">
+    <el-form ref="queryRef" :inline="true" :model="searchForm" v-show="showSearch" label-width="80px">
+      <el-form-item label="隐患类型" prop="hazardType">
+        <el-select v-model="searchForm.hazardType" placeholder="请选择隐患类型" clearable style="width: 160px">
+          <el-option v-for="d in hazardTypeDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="隐患等级" prop="hazardLevel">
+        <el-select v-model="searchForm.hazardLevel" placeholder="请选择隐患等级" clearable style="width: 160px">
+          <el-option v-for="d in hazardLevelDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="hazardStatus">
+        <el-select v-model="searchForm.hazardStatus" placeholder="请选择状态" clearable style="width: 160px">
+          <el-option v-for="d in hazardStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="报送状态" prop="isSubmitted">
+        <el-select v-model="searchForm.isSubmitted" placeholder="请选择报送状态" clearable style="width: 160px">
+          <el-option label="未报送" :value="0" />
+          <el-option label="已报送" :value="1" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="关联管网" prop="networkId">
+        <el-select-v2 v-model="searchForm.networkId" placeholder="请选择关联管网" clearable filterable style="width: 200px" :options="networkSelectOptions" @focus="loadNetworkOptions" />
+      </el-form-item>
+      <el-form-item label="关联设备" prop="equipmentId">
+        <el-select-v2 v-model="searchForm.equipmentId" placeholder="请选择关联设备" clearable filterable style="width: 200px" :options="equipmentSelectOptions" @focus="loadEquipmentOptions" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
+        <el-button icon="Refresh" @click="handleReset">重置</el-button>
+      </el-form-item>
+    </el-form>
 
-    <el-card class="table-card">
-      <el-table :data="tableData" border stripe v-loading="loading">
-        <el-table-column prop="hazardNo" label="隐患编号" width="120" />
-        <el-table-column prop="hazardType" label="隐患类型" width="100">
-          <template #default="{ row }">{{ getDictLabel(hazardTypeDict, row.hazardType) }}</template>
-        </el-table-column>
-        <el-table-column prop="hazardLevel" label="隐患等级" width="90">
-          <template #default="{ row }">
-            <el-tag :type="levelTag(row.hazardLevel)">{{ getDictLabel(hazardLevelDict, row.hazardLevel) }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="hazardDesc" label="隐患描述" min-width="180" show-overflow-tooltip />
-        <el-table-column prop="hazardSource" label="隐患来源" width="100">
-          <template #default="{ row }">{{ getDictLabel(hazardSourceDict, row.hazardSource) }}</template>
-        </el-table-column>
-        <el-table-column prop="hazardStatus" label="状态" width="90">
-          <template #default="{ row }">
-            <el-tag :type="statusTag(row.hazardStatus)">{{ getDictLabel(hazardStatusDict, row.hazardStatus) }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="隐患图片" width="90">
-          <template #default="{ row }">
-            <template v-if="getRowImageUrls(row).length">
-              <el-image
-                :src="getRowImageUrls(row)[0]"
-                fit="contain"
-                lazy
-                :preview-src-list="getRowImageUrls(row)"
-                preview-teleported
-                hide-on-click-modal
-                class="table-image"
-              />
-            </template>
-            <el-tag v-else type="info" size="small">无</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="关联管网" width="140">
-          <template #default="{ row }">{{ getNetworkName(row.networkId) }}</template>
-        </el-table-column>
-        <el-table-column label="关联设备" width="140">
-          <template #default="{ row }">{{ getEquipmentName(row.equipmentId) }}</template>
-        </el-table-column>
-        <el-table-column prop="reportUnit" label="上报单位" width="120" />
-        <el-table-column prop="reportTime" label="上报时间" width="160" />
-        <el-table-column label="操作" width="220" fixed="right">
-          <template #default="{ row }">
-            <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
-            <el-button type="info" size="small" @click="handleDetail(row)">详情</el-button>
-            <el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="loadData" />
+    </el-row>
+
+    <el-table :data="tableData" v-loading="loading">
+      <el-table-column prop="hazardNo" label="隐患编号" width="130" show-overflow-tooltip />
+      <el-table-column prop="hazardType" label="隐患类型" width="100" align="center">
+        <template #default="{ row }">{{ getDictLabel(hazardTypeDict, row.hazardType) }}</template>
+      </el-table-column>
+      <el-table-column prop="hazardLevel" label="隐患等级" width="100" align="center">
+        <template #default="{ row }">
+          <el-tag :type="levelTag(row.hazardLevel)">{{ getDictLabel(hazardLevelDict, row.hazardLevel) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="hazardDesc" label="隐患描述" min-width="180" show-overflow-tooltip />
+      <el-table-column prop="hazardSource" label="隐患来源" width="110" align="center">
+        <template #default="{ row }">{{ getDictLabel(hazardSourceDict, row.hazardSource) }}</template>
+      </el-table-column>
+      <el-table-column prop="hazardStatus" label="状态" width="100" align="center">
+        <template #default="{ row }">
+          <el-tag :type="statusTag(row.hazardStatus)">{{ getDictLabel(hazardStatusDict, row.hazardStatus) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="隐患图片" width="90" align="center">
+        <template #default="{ row }">
+          <template v-if="getRowImageUrls(row).length">
+            <el-image
+              :src="getRowImageUrls(row)[0]"
+              fit="contain"
+              lazy
+              :preview-src-list="getRowImageUrls(row)"
+              preview-teleported
+              hide-on-click-modal
+              class="table-image"
+            />
           </template>
-        </el-table-column>
-      </el-table>
-      <el-pagination
-        v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total"
-        layout="total, prev, pager, next, sizes" :page-sizes="[10, 20, 50]"
-        @change="loadData" style="margin-top: 16px; justify-content: flex-end"
-      />
-    </el-card>
+          <el-tag v-else type="info" size="small">无</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="关联管网" width="140" show-overflow-tooltip>
+        <template #default="{ row }">{{ getNetworkName(row.networkId) }}</template>
+      </el-table-column>
+      <el-table-column label="关联设备" width="140" show-overflow-tooltip>
+        <template #default="{ row }">{{ getEquipmentName(row.equipmentId) }}</template>
+      </el-table-column>
+      <el-table-column prop="reportUnit" label="上报单位" width="130" show-overflow-tooltip />
+      <el-table-column prop="reportTime" label="上报时间" width="170" align="center" />
+      <el-table-column label="操作" width="200" fixed="right" align="center" class-name="small-padding fixed-width">
+        <template #default="{ row }">
+          <el-button link type="primary" icon="Edit" @click="handleEdit(row)">修改</el-button>
+          <el-button link type="primary" icon="View" @click="handleDetail(row)">详情</el-button>
+          <el-button link type="danger" icon="Delete" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      v-model:page="pageNum"
+      v-model:limit="pageSize"
+      :page-sizes="[10, 20, 50]"
+      @pagination="loadData"
+    />
 
     <!-- 新增/编辑对话框 -->
-    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="750px" destroy-on-close>
+    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="750px" append-to-body destroy-on-close>
       <el-form :model="formData" ref="formRef" label-width="100px" :rules="rules">
         <el-row :gutter="16">
           <el-col :span="12">
@@ -207,8 +213,10 @@
         </el-row>
       </el-form>
       <template #footer>
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="handleSave">确定</el-button>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="handleSave">确 定</el-button>
+          <el-button @click="dialogVisible = false">取 消</el-button>
+        </div>
       </template>
     </el-dialog>
 
@@ -279,6 +287,8 @@ import { getDicts } from '@/api/system/dict/data'
 import { getPipeNetworkOptions, getEquipmentByTopLevelType } from '@/api/pipeNetwork/basic'
 
 const loading = ref(false)
+const showSearch = ref(true)
+const queryRef = ref(null)
 const tableData = ref([])
 const pageNum = ref(1)
 const pageSize = ref(10)
@@ -457,6 +467,7 @@ async function loadData() {
 }
 function handleSearch() { pageNum.value = 1; loadData() }
 function handleReset() {
+  queryRef.value?.resetFields()
   Object.assign(searchForm, { hazardType: '', hazardLevel: '', hazardStatus: '', isSubmitted: null, networkId: '', equipmentId: '' })
   handleSearch()
 }
@@ -575,9 +586,6 @@ onMounted(async () => {
 </script>
 
 <style scoped>
-.hazard-container { padding: 16px; }
-.search-card { margin-bottom: 16px; }
-
 /* 表格图片样式 */
 .table-image {
   width: 40px;

+ 143 - 161
src/views/subSystem/hidden/HazardOperationPage.vue

@@ -1,140 +1,139 @@
 <template>
-  <div class="workflow-page">
-    <el-card class="search-card" shadow="never">
-      <el-form :inline="true" :model="queryParams" class="search-form">
-        <el-form-item label="关键词">
-          <el-input
-            v-model="queryParams.keyword"
-            clearable
-            placeholder="隐患编号/描述/上报单位"
-            style="width: 220px"
-            @keyup.enter="handleSearch"
-          />
-        </el-form-item>
-        <el-form-item label="隐患类型">
-          <el-select v-model="queryParams.hazardType" clearable placeholder="全部" style="width: 140px">
-            <el-option v-for="item in hazardTypeDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="隐患等级">
-          <el-select v-model="queryParams.hazardLevel" clearable placeholder="全部" style="width: 130px">
-            <el-option v-for="item in hazardLevelDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="隐患来源">
-          <el-select v-model="queryParams.hazardSource" clearable placeholder="全部" style="width: 150px">
-            <el-option v-for="item in hazardSourceDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="报送状态">
-          <el-select v-model="queryParams.isSubmitted" clearable placeholder="全部" style="width: 120px">
-            <el-option label="未报送" :value="0" />
-            <el-option label="已报送" :value="1" />
-          </el-select>
-        </el-form-item>
-        <el-form-item v-if="mode !== 'submission'" label="治理状态">
-          <el-select v-model="queryParams.hazardStatus" clearable placeholder="全部" style="width: 130px">
-            <el-option v-for="item in hazardStatusDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
-          </el-select>
-        </el-form-item>
-        <el-form-item v-if="mode === 'submission'" label="时间类型">
-          <el-select v-model="queryParams.timeType" style="width: 140px">
-            <el-option label="隐患上报时间" value="report" />
-            <el-option label="报送时间" value="submit" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="时间范围">
-          <el-date-picker
-            v-model="dateRange"
-            type="daterange"
-            value-format="YYYY-MM-DD"
-            range-separator="至"
-            start-placeholder="开始日期"
-            end-placeholder="结束日期"
-            style="width: 250px"
-          />
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" @click="handleSearch">查询</el-button>
-          <el-button @click="handleReset">重置</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
-
-    <el-card class="table-card" shadow="never">
-      <el-table v-loading="loading" :data="tableData" border stripe>
-        <el-table-column prop="hazardNo" label="隐患编号" width="155" fixed="left" />
-        <el-table-column label="隐患类型" width="115">
-          <template #default="{ row }">{{ getDictLabel(hazardTypeDict, row.hazardType) }}</template>
-        </el-table-column>
-        <el-table-column label="隐患等级" width="105">
+  <div class="app-container">
+    <el-form ref="queryRef" :inline="true" :model="queryParams" v-show="showSearch" label-width="80px">
+      <el-form-item label="关键词" prop="keyword">
+        <el-input
+          v-model="queryParams.keyword"
+          clearable
+          placeholder="隐患编号/描述/上报单位"
+          style="width: 220px"
+          @keyup.enter="handleSearch"
+        />
+      </el-form-item>
+      <el-form-item label="隐患类型" prop="hazardType">
+        <el-select v-model="queryParams.hazardType" clearable placeholder="全部" style="width: 150px">
+          <el-option v-for="item in hazardTypeDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="隐患等级" prop="hazardLevel">
+        <el-select v-model="queryParams.hazardLevel" clearable placeholder="全部" style="width: 140px">
+          <el-option v-for="item in hazardLevelDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="隐患来源" prop="hazardSource">
+        <el-select v-model="queryParams.hazardSource" clearable placeholder="全部" style="width: 160px">
+          <el-option v-for="item in hazardSourceDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="报送状态" prop="isSubmitted">
+        <el-select v-model="queryParams.isSubmitted" clearable placeholder="全部" style="width: 140px">
+          <el-option label="未报送" :value="0" />
+          <el-option label="已报送" :value="1" />
+        </el-select>
+      </el-form-item>
+      <el-form-item v-if="mode !== 'submission'" label="治理状态" prop="hazardStatus">
+        <el-select v-model="queryParams.hazardStatus" clearable placeholder="全部" style="width: 140px">
+          <el-option v-for="item in hazardStatusDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item v-if="mode === 'submission'" label="时间类型" prop="timeType">
+        <el-select v-model="queryParams.timeType" style="width: 150px">
+          <el-option label="隐患上报时间" value="report" />
+          <el-option label="报送时间" value="submit" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="时间范围">
+        <el-date-picker
+          v-model="dateRange"
+          type="daterange"
+          value-format="YYYY-MM-DD"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          style="width: 250px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
+        <el-button icon="Refresh" @click="handleReset">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="loadData" />
+    </el-row>
+
+    <el-table v-loading="loading" :data="tableData">
+      <el-table-column prop="hazardNo" label="隐患编号" width="155" show-overflow-tooltip fixed="left" />
+      <el-table-column label="隐患类型" width="115" align="center">
+        <template #default="{ row }">{{ getDictLabel(hazardTypeDict, row.hazardType) }}</template>
+      </el-table-column>
+      <el-table-column label="隐患等级" width="105" align="center">
+        <template #default="{ row }">
+          <el-tag :type="levelTag(row.hazardLevel)">{{ getDictLabel(hazardLevelDict, row.hazardLevel) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="hazardDesc" label="隐患描述" min-width="220" show-overflow-tooltip />
+      <el-table-column label="隐患来源" width="125" align="center">
+        <template #default="{ row }">{{ getDictLabel(hazardSourceDict, row.hazardSource) || '-' }}</template>
+      </el-table-column>
+      <el-table-column prop="reportUnit" label="上报单位" width="150" show-overflow-tooltip />
+      <el-table-column prop="reportTime" label="上报时间" width="165" align="center" />
+
+      <template v-if="mode === 'submission'">
+        <el-table-column label="报送状态" width="110" align="center">
           <template #default="{ row }">
-            <el-tag :type="levelTag(row.hazardLevel)">{{ getDictLabel(hazardLevelDict, row.hazardLevel) }}</el-tag>
+            <el-tag :type="isSubmitted(row) ? 'success' : 'info'">{{ isSubmitted(row) ? '已报送' : '未报送' }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="hazardDesc" label="隐患描述" min-width="220" show-overflow-tooltip />
-        <el-table-column label="隐患来源" width="125">
-          <template #default="{ row }">{{ getDictLabel(hazardSourceDict, row.hazardSource) || '-' }}</template>
+        <el-table-column prop="submitTime" label="报送时间" width="165" align="center">
+          <template #default="{ row }">{{ row.submitTime || '-' }}</template>
         </el-table-column>
-        <el-table-column prop="reportUnit" label="上报单位" width="150" show-overflow-tooltip />
-        <el-table-column prop="reportTime" label="上报时间" width="165" />
-
-        <template v-if="mode === 'submission'">
-          <el-table-column label="报送状态" width="100">
-            <template #default="{ row }">
-              <el-tag :type="isSubmitted(row) ? 'success' : 'info'">{{ isSubmitted(row) ? '已报送' : '未报送' }}</el-tag>
-            </template>
-          </el-table-column>
-          <el-table-column prop="submitTime" label="报送时间" width="165">
-            <template #default="{ row }">{{ row.submitTime || '-' }}</template>
-          </el-table-column>
-        </template>
-
-        <template v-else>
-          <el-table-column label="治理状态" width="110">
-            <template #default="{ row }">
-              <el-tag :type="statusTag(row.hazardStatus)">{{ getDictLabel(hazardStatusDict, row.hazardStatus) }}</el-tag>
-            </template>
-          </el-table-column>
-          <el-table-column prop="rectifyPerson" label="整改责任人" width="120">
-            <template #default="{ row }">{{ row.rectifyPerson || '-' }}</template>
-          </el-table-column>
-          <el-table-column prop="rectifyDeadline" label="整改期限" width="120">
-            <template #default="{ row }">{{ row.rectifyDeadline || '-' }}</template>
-          </el-table-column>
-          <el-table-column prop="rectifyFeedback" label="治理反馈" min-width="180" show-overflow-tooltip>
-            <template #default="{ row }">{{ row.rectifyFeedback || '-' }}</template>
-          </el-table-column>
-        </template>
+      </template>
 
-        <el-table-column label="操作" :width="mode === 'submission' ? 190 : 180" fixed="right">
+      <template v-else>
+        <el-table-column label="治理状态" width="110" align="center">
           <template #default="{ row }">
-            <el-button type="primary" link @click="handleDetail(row)">详情</el-button>
-            <el-button
-              v-if="mode === 'submission'"
-              type="success"
-              link
-              :disabled="isSubmitted(row)"
-              @click="handleSubmit(row)"
-            >
-              {{ isSubmitted(row) ? '已报送' : '报送' }}
-            </el-button>
-            <el-button v-else type="warning" link @click="handleFeedback(row)">治理反馈</el-button>
+            <el-tag :type="statusTag(row.hazardStatus)">{{ getDictLabel(hazardStatusDict, row.hazardStatus) }}</el-tag>
           </template>
         </el-table-column>
-      </el-table>
-
-      <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"
-        class="pagination"
-        @current-change="loadData"
-        @size-change="handleSizeChange"
-      />
-    </el-card>
+        <el-table-column prop="rectifyPerson" label="整改责任人" width="120" align="center">
+          <template #default="{ row }">{{ row.rectifyPerson || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="rectifyDeadline" label="整改期限" width="120" align="center">
+          <template #default="{ row }">{{ row.rectifyDeadline || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="rectifyFeedback" label="治理反馈" min-width="180" show-overflow-tooltip>
+          <template #default="{ row }">{{ row.rectifyFeedback || '-' }}</template>
+        </el-table-column>
+      </template>
+
+      <el-table-column label="操作" :width="mode === 'submission' ? 190 : 180" fixed="right" align="center" class-name="small-padding fixed-width">
+        <template #default="{ row }">
+          <el-button link type="primary" icon="View" @click="handleDetail(row)">详情</el-button>
+          <el-button
+            v-if="mode === 'submission'"
+            link
+            type="success"
+            icon="Promotion"
+            :disabled="isSubmitted(row)"
+            @click="handleSubmit(row)"
+          >
+            {{ isSubmitted(row) ? '已报送' : '报送' }}
+          </el-button>
+          <el-button v-else link type="warning" icon="Edit" @click="handleFeedback(row)">治理反馈</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      v-model:page="pageNum"
+      v-model:limit="pageSize"
+      :page-sizes="[10, 20, 50]"
+      @pagination="loadData"
+    />
 
     <el-drawer v-model="detailVisible" title="隐患详情" size="760px" direction="rtl">
       <el-descriptions v-if="detailData" :column="2" border>
@@ -179,7 +178,7 @@
       </el-descriptions>
     </el-drawer>
 
-    <el-dialog v-model="feedbackVisible" title="隐患治理反馈" width="720px" destroy-on-close>
+    <el-dialog v-model="feedbackVisible" title="隐患治理反馈" width="720px" append-to-body destroy-on-close>
       <el-form ref="feedbackFormRef" :model="feedbackForm" :rules="feedbackRules" label-width="105px">
         <el-alert
           :title="`${feedbackForm.hazardNo || ''} ${feedbackForm.hazardDesc || ''}`"
@@ -245,8 +244,10 @@
         </el-row>
       </el-form>
       <template #footer>
-        <el-button @click="feedbackVisible = false">取消</el-button>
-        <el-button type="primary" :loading="saving" @click="saveFeedback">保存反馈</el-button>
+        <div class="dialog-footer">
+          <el-button type="primary" :loading="saving" @click="saveFeedback">保存反馈</el-button>
+          <el-button @click="feedbackVisible = false">取 消</el-button>
+        </div>
       </template>
     </el-dialog>
   </div>
@@ -268,6 +269,8 @@ const props = defineProps({
 
 const loading = ref(false)
 const saving = ref(false)
+const showSearch = ref(true)
+const queryRef = ref(null)
 const tableData = ref([])
 const pageNum = ref(1)
 const pageSize = ref(10)
@@ -332,7 +335,8 @@ function levelTag(value) {
   const label = String(getDictLabel(hazardLevelDict.value, value) || '')
   if (label.includes('重大') || String(value) === '0') return 'danger'
   if (label.includes('较大') || String(value) === '1') return 'warning'
-  if (label.includes('一般') || String(value) === '2') return 'primary'
+  // Element Plus 没有 primary 类型,空串回落成默认色
+  if (label.includes('一般') || String(value) === '2') return ''
   return 'info'
 }
 
@@ -429,6 +433,7 @@ function handleSearch() {
 }
 
 function handleReset() {
+  queryRef.value?.resetFields()
   Object.assign(queryParams, {
     keyword: '',
     hazardType: '',
@@ -442,11 +447,6 @@ function handleReset() {
   handleSearch()
 }
 
-function handleSizeChange() {
-  pageNum.value = 1
-  loadData()
-}
-
 async function fetchDetail(row) {
   try {
     const response = await getHazardById(row.hazardId)
@@ -535,30 +535,6 @@ onMounted(async () => {
 </script>
 
 <style scoped>
-.workflow-page {
-  min-height: 100%;
-  padding: 16px;
-  background: #f5f7fa;
-}
-
-.search-card {
-  margin-bottom: 14px;
-  border: 1px solid #e1e8ef;
-}
-
-.search-form :deep(.el-form-item) {
-  margin-bottom: 10px;
-}
-
-.table-card {
-  border: 1px solid #e1e8ef;
-}
-
-.pagination {
-  justify-content: flex-end;
-  margin-top: 16px;
-}
-
 .detail-images {
   display: flex;
   flex-wrap: wrap;
@@ -575,4 +551,10 @@ onMounted(async () => {
   margin-bottom: 20px;
 }
 
+.dialog-footer {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 10px;
+}
 </style>

+ 1 - 14
src/views/subSystem/hidden/yhpc.vue

@@ -1,20 +1,7 @@
 <template>
-  <div class="inspection-page">
-    <HazardList />
-  </div>
+  <HazardList />
 </template>
 
 <script setup>
 import HazardList from './HazardList.vue'
 </script>
-
-<style scoped>
-.inspection-page {
-  min-height: 100%;
-  background: #f5f7fa;
-}
-
-:deep(.hazard-container) {
-  padding-top: 12px;
-}
-</style>

+ 22 - 6
src/views/subSystem/manholeCover/alarm/alarmSanalysis.vue

@@ -155,6 +155,7 @@ import {
   ZoomOut
 } from '@element-plus/icons-vue'
 import {ElMessage} from 'element-plus'
+import {convertCoord} from '@/utils/coordTransform'
 import {getManholeAlarmDeviceDetail, getManholeAlarmDeviceList, getManholeLayerData} from '@/api/pipeNetwork/basic'
 
 // 树复选框默认勾选全部状态
@@ -323,6 +324,21 @@ function isValidCoordinate(lng, lat) {
   return lng !== null && lat !== null && lng >= 70 && lng <= 140 && lat >= 3 && lat <= 55
 }
 
+/**
+ * 业务坐标(WGS84)转百度坐标点(BD-09)
+ *
+ * 设备台账的 longitude/latitude 存的是 WGS84,百度地图要 BD-09,
+ * 直接传原值会整体偏移约 1.1 公里,必须转换后再构建 Point
+ *
+ * @param lng 业务经度
+ * @param lat 业务纬度
+ * @returns {BMapGL.Point} 百度坐标点
+ */
+function toBdPoint(lng, lat) {
+  const [bdLng, bdLat] = convertCoord(Number(lng), Number(lat))
+  return new window.BMapGL.Point(bdLng, bdLat)
+}
+
 function getThresholdTextByCode(raw, warningCode) {
   const list = Array.isArray(raw?.warningThresholdList) ? raw.warningThresholdList : []
   const target = list.find(item => item.warningCode === warningCode)
@@ -508,14 +524,14 @@ function clearMapOverlays() {
 function fitMapViewport() {
   if (!mapInstance || !window.BMapGL) return
   if (!displayDevices.value.length) {
-    mapInstance.centerAndZoom(new window.BMapGL.Point(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 14)
+    mapInstance.centerAndZoom(toBdPoint(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 14)
     return
   }
   const points = displayDevices.value
     .filter(device => isValidCoordinate(device.lng, device.lat))
-    .map(device => new window.BMapGL.Point(device.lng, device.lat))
+    .map(device => toBdPoint(device.lng, device.lat))
   if (!points.length) {
-    mapInstance.centerAndZoom(new window.BMapGL.Point(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 14)
+    mapInstance.centerAndZoom(toBdPoint(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 14)
     return
   }
   mapInstance.setViewport(points)
@@ -537,7 +553,7 @@ function renderMapOverlays() {
   
   validDevices.forEach((device) => {
     console.log('[renderMapOverlays] 渲染点位:', device.name, 'lng:', device.lng, 'lat:', device.lat)
-    const point = new window.BMapGL.Point(device.lng, device.lat)
+    const point = toBdPoint(device.lng, device.lat)
     const isActive = selectedDevice.value?.equipmentId === device.equipmentId
     const label = new window.BMapGL.Label(getMarkerHtml(device, isActive), {
       position: point,
@@ -630,7 +646,7 @@ async function initMap() {
       mapInstance.enableScrollWheelZoom(true)
       mapInstance.setMinZoom(11)
       mapInstance.setMaxZoom(18)
-      mapInstance.centerAndZoom(new window.BMapGL.Point(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 14)
+      mapInstance.centerAndZoom(toBdPoint(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 14)
       mapInstance.addEventListener('click', () => {
         drawerVisible.value = false
       })
@@ -683,7 +699,7 @@ async function openDeviceDetail(device) {
       const index = mockManholes.value.findIndex(item => item.equipmentId === device.equipmentId)
       selectedDevice.value = normalizeDevice(res.data, index, device)
       if (mapInstance && window.BMapGL && isValidCoordinate(selectedDevice.value.lng, selectedDevice.value.lat)) {
-        mapInstance.panTo(new window.BMapGL.Point(selectedDevice.value.lng, selectedDevice.value.lat))
+        mapInstance.panTo(toBdPoint(selectedDevice.value.lng, selectedDevice.value.lat))
       }
       renderMap()
     }

+ 12 - 4
src/views/subSystem/manholeCover/device/deviceApicture.vue

@@ -128,6 +128,8 @@ import { Location, Connection, WarningFilled, Search, Close } from '@element-plu
 import { getManholeDevicePage, getManholeAlarmDeviceList } from '@/api/pipeNetwork/basic'
 import locationIcon from '@/assets/images/location.png'
 import { ElMessage } from 'element-plus'
+// 台账存 WGS84,百度地图是 BD-09,不转换会偏约 1 公里
+import { convertCoord } from '@/utils/coordTransform'
 
 // ==================== 分页查询(接口数据) ====================
 const searchKeyword = ref('')
@@ -211,7 +213,7 @@ function selectDevice(device) {
   selectedDevice.value = device
   // 地图中心定位到该设备(无坐标则不移动)
   if (mapInstance && device.lng && device.lat) {
-    mapInstance.centerAndZoom(new BMapGL.Point(device.lng, device.lat), 16)
+    mapInstance.centerAndZoom(toBdPoint(device.lng, device.lat), 16)
   }
   highlightMarker(device.id)
 }
@@ -225,9 +227,15 @@ function clearSelected() {
 let mapInstance = null
 const markerMap = new Map()
 
-// 沅陵县中心坐标
+// 沅陵县中心坐标(WGS84,展示前统一转 BD-09)
 const YUANLING_CENTER = { lng: 110.393, lat: 28.452 }
 
+/** 业务坐标(WGS84)→ 百度坐标点 */
+function toBdPoint(lng, lat) {
+  const [bdLng, bdLat] = convertCoord(Number(lng), Number(lat))
+  return new BMapGL.Point(bdLng, bdLat)
+}
+
 function initMap() {
   const container = document.getElementById('deviceApictureMap')
   if (!container || typeof BMapGL === 'undefined') {
@@ -236,7 +244,7 @@ function initMap() {
   }
   try {
     mapInstance = new BMapGL.Map('deviceApictureMap')
-    mapInstance.centerAndZoom(new BMapGL.Point(YUANLING_CENTER.lng, YUANLING_CENTER.lat), 13)
+    mapInstance.centerAndZoom(toBdPoint(YUANLING_CENTER.lng, YUANLING_CENTER.lat), 13)
     mapInstance.enableScrollWheelZoom(true)
   } catch (e) {
     console.error('百度地图初始化失败:', e)
@@ -254,7 +262,7 @@ function getStatusColor(status) {
 function addMapMarker(device) {
   if (!mapInstance || device.lng == null || device.lat == null) return
   const color = getStatusColor(device.status)
-  const bPoint = new BMapGL.Point(device.lng, device.lat)
+  const bPoint = toBdPoint(device.lng, device.lat)
   const isAttention = device.status === 'alert' || device.status === 'offline'
   const attentionText = device.status === 'alert' ? '告警' : '离线'
 

+ 230 - 46
src/views/subSystem/manholeCover/device/statistics.vue

@@ -1,6 +1,21 @@
 <!--监测设备统计分析-->
 <template>
   <div class="device-statistics-dashboard">
+    <!-- 顶部概览:三项关键指标 -->
+    <div class="summary-row">
+      <div v-for="item in summaryItems" :key="item.label" class="summary-card">
+        <div class="summary-icon" :style="{ background: item.bg, color: item.color }">
+          <el-icon>
+            <component :is="item.icon" />
+          </el-icon>
+        </div>
+        <div class="summary-body">
+          <div class="summary-value">{{ item.value }}<span class="summary-unit">{{ item.unit }}</span></div>
+          <div class="summary-label">{{ item.label }}</div>
+        </div>
+      </div>
+    </div>
+
     <!-- 图表展示区域:左右两列 -->
     <div class="charts-container">
       <!-- 左侧:区域分布柱状图 -->
@@ -10,14 +25,23 @@
             <el-icon>
               <Location />
             </el-icon>
-            设备区域分布(柱状图)
+            设备区域分布
           </span>
-          <el-radio-group v-model="barChartType" size="small">
+          <el-radio-group v-model="barChartType" size="small" :disabled="!hasRegionData">
             <el-radio-button label="count">设备数量</el-radio-button>
             <el-radio-button label="percent">占比 (%)</el-radio-button>
           </el-radio-group>
         </div>
-        <div ref="regionBarRef" class="chart-wrapper"></div>
+        <div class="chart-body">
+          <div ref="regionBarRef" class="chart-wrapper"></div>
+          <div v-if="!hasRegionData" class="empty-tip empty-tip--bar">
+            <el-icon class="empty-icon">
+              <Histogram />
+            </el-icon>
+            <p class="empty-title">暂无设备数据</p>
+            <p class="empty-desc">当前筛选条件下没有窨井设备</p>
+          </div>
+        </div>
       </div>
 
       <!-- 右侧:设备类型分布饼图 -->
@@ -27,14 +51,23 @@
             <el-icon>
               <PieChart />
             </el-icon>
-            设备类型分布(饼图)
+            设备类型分布
           </span>
-          <el-radio-group v-model="pieChartType" size="small">
+          <el-radio-group v-model="pieChartType" size="small" :disabled="!hasTypeData">
             <el-radio-button label="normal">标准饼图</el-radio-button>
             <el-radio-button label="doughnut">环形图</el-radio-button>
           </el-radio-group>
         </div>
-        <div ref="typePieRef" class="chart-wrapper"></div>
+        <div class="chart-body">
+          <div ref="typePieRef" class="chart-wrapper"></div>
+          <div v-if="!hasTypeData" class="empty-tip">
+            <el-icon class="empty-icon">
+              <PieChart />
+            </el-icon>
+            <p class="empty-title">暂无设备数据</p>
+            <p class="empty-desc">设备台账中还没有窨井设备</p>
+          </div>
+        </div>
       </div>
     </div>
   </div>
@@ -42,7 +75,7 @@
 
 <script setup>
 import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
-import { Location, PieChart } from '@element-plus/icons-vue'
+import { Location, PieChart, Histogram, Monitor } from '@element-plus/icons-vue'
 import { getManholeDeviceStatistics } from '@/api/pipeNetwork/basic'
 import { ElMessage } from 'element-plus'
 import * as echarts from 'echarts'
@@ -52,6 +85,7 @@ const barChartType = ref('count')   // count / percent
 const pieChartType = ref('normal')  // normal / doughnut
 
 // 统计数据(来自接口)
+const totalDevices = ref(0)
 const regionDistribution = ref([])
 const typeDistribution = ref([])
 
@@ -61,25 +95,46 @@ let typePieChart = null
 const regionBarRef = ref(null)
 const typePieRef = ref(null)
 
+// 无数据时仍展示的区域维度,取自 DistrictEnum(0-城南 1-城北 2-太常)
+const DEFAULT_REGIONS = ['城南', '城北', '太常']
+
+const hasRegionData = computed(() => regionDistribution.value.some(item => Number(item.count) > 0))
+const hasTypeData = computed(() => typeDistribution.value.some(item => Number(item.count) > 0))
+
+// 顶部概览指标
+const summaryItems = computed(() => {
+  const regionCount = regionDistribution.value.filter(item => Number(item.count) > 0).length
+  const typeCount = typeDistribution.value.filter(item => Number(item.count) > 0).length
+  return [
+    { label: '设备总数', value: totalDevices.value, unit: '台', icon: Monitor, color: '#409eff', bg: '#ecf5ff' },
+    { label: '覆盖区域', value: regionCount, unit: '个', icon: Location, color: '#67c23a', bg: '#f0f9eb' },
+    { label: '设备类型', value: typeCount, unit: '种', icon: PieChart, color: '#e6a23c', bg: '#fdf6ec' }
+  ]
+})
+
 // 加载统计数据(默认全部窨井设备)
 async function loadStatistics() {
   try {
     const res = await getManholeDeviceStatistics({})
+    totalDevices.value = Number(res.data?.total) || 0
     regionDistribution.value = res.data?.regionDistribution || []
     typeDistribution.value = res.data?.typeDistribution || []
   } catch (e) {
     ElMessage.error('统计数据加载失败')
+  } finally {
+    // 接口失败或返回空时也要重绘,避免图表区停留在初始空白
+    renderCharts()
   }
 }
 
 // 柱状图配置:设备区域分布(数量/占比)
 const regionBarOption = computed(() => {
-  const regions = regionDistribution.value.map(item => item.name)
-  const counts = regionDistribution.value.map(item => item.count)
-  const percentages = regionDistribution.value.map(item => item.percent)
+  const hasData = hasRegionData.value
+  const regions = hasData ? regionDistribution.value.map(item => item.name) : DEFAULT_REGIONS
+  const counts = hasData ? regionDistribution.value.map(item => Number(item.count) || 0) : DEFAULT_REGIONS.map(() => 0)
+  const percentages = hasData ? regionDistribution.value.map(item => Number(item.percent) || 0) : DEFAULT_REGIONS.map(() => 0)
 
   const seriesData = barChartType.value === 'count' ? counts : percentages
-  const yAxisName = barChartType.value === 'count' ? '设备数量 (台)' : '占比 (%)'
   const seriesName = barChartType.value === 'count' ? '设备数量' : '设备占比'
 
   return {
@@ -95,36 +150,44 @@ const regionBarOption = computed(() => {
       }
     },
     grid: {
-      left: '3%',
-      right: '4%',
-      bottom: '3%',
+      left: 10,
+      right: 20,
+      top: 36,
+      bottom: 6,
       containLabel: true
     },
+    // 空态只保留区域分类轴,y 轴 0~1 的刻度反而显得空荡
+    yAxis: {
+      type: 'value',
+      show: hasData,
+      minInterval: 1,
+      axisLabel: { color: '#909399', fontSize: 11 },
+      splitLine: { lineStyle: { color: '#f0f2f5' } }
+    },
     xAxis: {
       type: 'category',
       data: regions,
-      axisLabel: { rotate: 30, interval: 0, fontSize: 11 }
-    },
-    yAxis: {
-      type: 'value',
-      name: yAxisName,
-      nameLocation: 'middle',
-      nameGap: 45
+      axisTick: { alignWithLabel: true, show: hasData },
+      axisLine: { lineStyle: { color: '#dcdfe6' } },
+      axisLabel: { interval: 0, fontSize: 12, color: hasData ? '#606266' : '#c0c4cc' }
     },
     series: [
       {
         name: seriesName,
         type: 'bar',
         data: seriesData,
+        barMaxWidth: 48,
         itemStyle: {
           borderRadius: [4, 4, 0, 0],
           color: '#409eff',
-          shadowColor: 'rgba(0,0,0,0.1)',
+          shadowColor: 'rgba(64,158,255,0.25)',
           shadowBlur: 6
         },
         label: {
-          show: true,
+          show: hasData,
           position: 'top',
+          fontSize: 12,
+          color: '#606266',
           formatter: ({ value }) => barChartType.value === 'count' ? value : value + '%'
         }
       }
@@ -134,27 +197,32 @@ const regionBarOption = computed(() => {
 
 // 饼图配置:设备类型分布(标准/环形)
 const typePieOption = computed(() => {
+  const hasData = hasTypeData.value
   const pieData = typeDistribution.value.map(item => ({
     name: item.name,
-    value: item.count
+    value: Number(item.count) || 0
   }))
 
   return {
-    tooltip: {
-      trigger: 'item',
-      formatter: '{b}: {c}台 ({d}%)'
-    },
+    tooltip: { show: hasData, trigger: 'item', formatter: '{b}: {c}台 ({d}%)' },
     legend: {
+      show: hasData,
       orient: 'vertical',
       left: 'left',
+      top: 'middle',
+      itemWidth: 10,
+      itemHeight: 10,
+      itemGap: 12,
+      textStyle: { fontSize: 12, color: '#606266' },
       data: pieData.map(d => d.name)
     },
-    series: [
+    // 无数据时不挂 series,否则 echarts 会在中心留一个灰色空心圆压住提示文字
+    series: hasData ? [
       {
         name: '设备类型占比',
         type: 'pie',
-        radius: pieChartType.value === 'doughnut' ? ['40%', '70%'] : '55%',
-        center: ['50%', '50%'],
+        radius: pieChartType.value === 'doughnut' ? ['42%', '64%'] : '64%',
+        center: ['62%', '52%'],
         data: pieData,
         emphasis: {
           scale: true,
@@ -163,8 +231,11 @@ const typePieOption = computed(() => {
         label: {
           show: true,
           formatter: '{b}: {d}%',
+          fontSize: 12,
+          color: '#606266',
           position: 'outside'
         },
+        labelLine: { length: 10, length2: 8 },
         itemStyle: {
           borderRadius: 6,
           borderColor: '#fff',
@@ -172,15 +243,18 @@ const typePieOption = computed(() => {
         },
         color: ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#9b59b6', '#1abc9c']
       }
-    ]
+    ] : []
   }
 })
 
-// 图表配置变化时刷新
-watch([regionBarOption, typePieOption], () => {
+// 统一渲染,空态也要走一次,否则接口失败时图表区一片空白
+function renderCharts() {
   if (regionBarChart) regionBarChart.setOption(regionBarOption.value, true)
   if (typePieChart) typePieChart.setOption(typePieOption.value, true)
-}, { deep: true })
+}
+
+// 图表配置变化时刷新
+watch([regionBarOption, typePieOption], renderCharts, { deep: true })
 
 // 窗口变化自适应(防抖)
 let resizeTimer = null
@@ -196,6 +270,8 @@ onMounted(async () => {
   await nextTick()
   regionBarChart = echarts.init(regionBarRef.value)
   typePieChart = echarts.init(typePieRef.value)
+  // 先铺空态,接口慢或失败时也不会留一片空白
+  renderCharts()
   window.addEventListener('resize', handleResize)
   loadStatistics()
 })
@@ -210,25 +286,78 @@ onBeforeUnmount(() => {
 
 <style scoped>
 .device-statistics-dashboard {
-  padding: 20px;
+  padding: 20px 24px;
   background: #f5f7fb;
   min-height: 100vh;
   box-sizing: border-box;
   font-family: 'Segoe UI', 'PingFang SC', Roboto, Helvetica, Arial, sans-serif;
 }
 
+/* ==================== 顶部概览 ==================== */
+.summary-row {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 20px;
+  margin-bottom: 20px;
+}
+
+.summary-card {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  padding: 18px 20px;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+}
+
+.summary-icon {
+  width: 44px;
+  height: 44px;
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 10px;
+  font-size: 22px;
+}
+
+.summary-body {
+  min-width: 0;
+}
+
+.summary-value {
+  font-size: 24px;
+  font-weight: 600;
+  line-height: 1.2;
+  color: #303133;
+}
+
+.summary-unit {
+  margin-left: 3px;
+  font-size: 13px;
+  font-weight: 400;
+  color: #909399;
+}
+
+.summary-label {
+  margin-top: 2px;
+  font-size: 13px;
+  color: #909399;
+}
+
+/* ==================== 图表区域 ==================== */
 .charts-container {
   display: grid;
   grid-template-columns: 1fr 1fr;
   gap: 20px;
-  /* 随视口高度自适应,占满剩余空间 */
-  height: calc(100vh - 40px);
-  min-height: 420px;
+  /* 固定高度,避免图表被拉伸到整个视口 */
+  height: 380px;
 }
 
 .chart-card {
-  background: white;
-  border-radius: 16px;
+  background: #fff;
+  border-radius: 12px;
   box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
   overflow: hidden;
   display: flex;
@@ -240,8 +369,8 @@ onBeforeUnmount(() => {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  padding: 16px 20px;
-  border-bottom: 1px solid #ebeef5;
+  padding: 14px 18px;
+  border-bottom: 1px solid #f0f2f5;
   flex-shrink: 0;
 }
 
@@ -250,14 +379,69 @@ onBeforeUnmount(() => {
   align-items: center;
   gap: 8px;
   font-weight: 600;
-  font-size: 16px;
+  font-size: 15px;
   color: #303133;
 }
 
-/* 图表容器占满卡片剩余高度 */
-.chart-wrapper {
+.chart-body {
+  position: relative;
   flex: 1;
   min-height: 0;
+}
+
+.chart-wrapper {
   width: 100%;
+  height: 100%;
+}
+
+/* 空态提示:叠在图表上方,柱状图保留底部分类轴 */
+.empty-tip {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+  pointer-events: none;
+}
+
+/* 柱状图空态让出 x 轴标签的高度 */
+.empty-tip--bar {
+  padding-bottom: 34px;
+}
+
+.empty-icon {
+  margin-bottom: 4px;
+  font-size: 40px;
+  color: #dcdfe6;
+}
+
+.empty-title {
+  margin: 0;
+  font-size: 14px;
+  color: #909399;
+}
+
+.empty-desc {
+  margin: 0;
+  font-size: 12px;
+  color: #c0c4cc;
+}
+
+/* 窄屏改为上下排列 */
+@media (max-width: 1280px) {
+  .summary-row {
+    grid-template-columns: 1fr;
+  }
+
+  .charts-container {
+    grid-template-columns: 1fr;
+    height: auto;
+  }
+
+  .chart-card {
+    height: 340px;
+  }
 }
 </style>

+ 190 - 43
src/views/subSystem/manholeCover/home/index.vue

@@ -19,7 +19,10 @@
 
       <!-- 告警类型统计(15天内) -->
       <div class="panel">
-        <h3 class="panel-title">告警类型统计(15天内)</h3>
+        <h3 class="panel-title">
+          告警类型统计(15天内)
+          <span class="title-count">{{ alarmRangeText }}</span>
+        </h3>
         <div ref="alarmChartRef" class="chart" v-loading="loading.alarm" />
       </div>
 
@@ -58,15 +61,13 @@
         <h3 class="panel-title">设备列表 <span class="title-count">共 {{ filteredDevices.length }} 台</span></h3>
         <div class="filter-bar">
           <el-select v-model="deviceTypeFilter" size="small" placeholder="设备类型" clearable style="width: 120px">
-            <el-option label="供水窨井" value="5" />
-            <el-option label="排水窨井" value="9" />
-            <el-option label="燃气窨井" value="10" />
+            <el-option v-for="item in deviceTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
           </el-select>
         </div>
         <el-table :data="filteredDevices" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.device">
           <el-table-column prop="name" label="设备名称" min-width="100" show-overflow-tooltip />
           <el-table-column label="类型" width="70">
-            <template #default="{ row }">{{ typeText(row.typeId) }}</template>
+            <template #default="{ row }">{{ typeText(row.typeId, row.typeName) }}</template>
           </el-table-column>
           <el-table-column label="电量" width="60">
             <template #default="{ row }">
@@ -108,6 +109,8 @@ import { ORDER_STATUS_GROUP } from '@/views/subSystem/pipeNetwork/ops/workOrder/
 import { getAlarmDataPage } from '@/api/drainage'
 import { ElMessage } from 'element-plus'
 import locationIcon from '@/assets/images/location.png'
+// 台账存 WGS84(设备表单选点经 bd09ToWgs84 回填),百度地图必须转 BD-09 再画
+import { convertCoord } from '@/utils/coordTransform'
 
 // ==================== 状态 ====================
 const loading = reactive({ work: false, alarm: false, device: false })
@@ -117,6 +120,15 @@ const workOrders = ref([])
 const allDevices = ref([])
 const dynamicLogs = ref([])
 const deviceTypeFilter = ref('')
+const alarmRangeText = ref(formatAlarmRange())
+
+/** 告警统计区间文案,空数据时也展示,避免面板看不出统计范围 */
+function formatAlarmRange() {
+  const end = new Date()
+  const start = new Date(end.getTime() - ALARM_RANGE_DAYS * 24 * 3600 * 1000)
+  const label = date => `${date.getMonth() + 1}-${String(date.getDate()).padStart(2, '0')}`
+  return `${label(start)} ~ ${label(end)}`
+}
 
 // 图表引用
 const alarmChartRef = ref(null)
@@ -125,7 +137,8 @@ const deviceTypeChartRef = ref(null)
 let alarmChart = null, areaChart = null, deviceTypeChart = null
 
 // ==================== 常量映射 ====================
-const TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
+/** 历史遗留窨井类型编码,仅用于后端未返回 typeName 时兜底展示 */
+const LEGACY_TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
 
 // 所属片区:后端 DistrictEnum(0-城南 1-城北 2-太常)
 const DISTRICT_MAP = { 0: '城南', 1: '城北', 2: '太常' }
@@ -140,7 +153,16 @@ const priorityClass = (l) => ({ 0: 'high', 1: 'medium', 2: 'low' }[l] || 'low')
 const TODO_ORDER_STATUS = ORDER_STATUS_GROUP.todo.join(',')
 /** 设备父类别ID(equipment_type.parent_id),与工单列表页保持同一口径 */
 const DEVICE_TYPE_PARENT_ID = '4'
-const typeText = (id) => TYPE_MAP[id] || '-'
+/** 窨井设备大类根ID(EquipmentTypeEnum.ROOT_MANHOLE_DEVICE),报警查询按大类隔离 */
+const MANHOLE_ROOT_TYPE_ID = 4
+/** 报警查询后端限制 pageSize 不超过 100 */
+const ALARM_PAGE_SIZE = 100
+const ALARM_MAX_PAGES = 3
+/** 告警统计窗口(天) */
+const ALARM_RANGE_DAYS = 15
+/** 无设备数据时仍展示的区域维度,取自 DistrictEnum(0-城南 1-城北 2-太常) */
+const DEFAULT_DISTRICTS = ['城南', '城北', '太常']
+const typeText = (id, name) => name || LEGACY_TYPE_MAP[id] || (id ? `类型${id}` : '-')
 const statusText = (s) => ({ normal: '在线', alert: '告警', offline: '离线' }[s] || s)
 const batteryTag = (b) => {
   if (b == null) return 'info'
@@ -150,9 +172,20 @@ const batteryTag = (b) => {
 }
 
 // ==================== 筛选后设备列表 ====================
+/** 设备类型下拉项:跟着台账实际类型走,不再写死 5/9/10 */
+const deviceTypeOptions = computed(() => {
+  const map = new Map()
+  allDevices.value.forEach(d => {
+    const value = String(d.typeId ?? '')
+    if (!value || map.has(value)) return
+    map.set(value, { value, label: typeText(d.typeId, d.typeName) })
+  })
+  return Array.from(map.values())
+})
+
 const filteredDevices = computed(() => {
   if (!deviceTypeFilter.value) return allDevices.value
-  return allDevices.value.filter(d => d.typeId === deviceTypeFilter.value)
+  return allDevices.value.filter(d => String(d.typeId) === deviceTypeFilter.value)
 })
 
 // ==================== 数据加载 ====================
@@ -197,6 +230,9 @@ async function loadDevices() {
     refreshMarkers()
   } catch (e) {
     ElMessage.error('设备数据加载失败')
+    // 加载失败也铺出空态骨架,面板不留白
+    renderAreaChart()
+    renderDeviceTypeChart()
   } finally {
     loading.device = false
   }
@@ -219,33 +255,63 @@ async function loadWorkOrders() {
   }
 }
 
+function formatDateTime(date) {
+  const pad = value => String(value).padStart(2, '0')
+  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
+}
+
+/** 报警类型展示:优先报警内容,其次报警编码 */
+function alarmTypeText(record) {
+  return record.alarmContent || record.warningCode || '其他告警'
+}
+
+/**
+ * 拉取近 15 天的窨井报警
+ *
+ * rootTypeId 是报警查询的必填项(后端校验「设备大类不能为空」),
+ * pageSize 上限 100,因此按页累加,最多取 3 页用于首页统计。
+ */
+async function fetchRecentAlarms() {
+  const params = {
+    rootTypeId: MANHOLE_ROOT_TYPE_ID,
+    startTime: formatDateTime(new Date(Date.now() - 15 * 24 * 3600 * 1000))
+  }
+  const records = []
+  for (let page = 1; page <= ALARM_MAX_PAGES; page++) {
+    const res = await getAlarmDataPage(page, ALARM_PAGE_SIZE, params)
+    const pageRecords = res.rows || res.data?.records || res.data || []
+    records.push(...pageRecords)
+    if (pageRecords.length < ALARM_PAGE_SIZE) break
+  }
+  return records
+}
+
 async function loadAlarmData() {
   loading.alarm = true
+  alarmRangeText.value = formatAlarmRange()
   try {
-    const res = await getAlarmDataPage(1, 500, {})
-    const records = res.rows || res.data?.records || res.data || []
-    const now = new Date()
-    const fifteenDaysAgo = new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000)
-    // 筛选15天内的告警
-    const recentAlarms = records.filter(r => {
-      const t = new Date(r.createTime)
-      return t >= fifteenDaysAgo
-    })
-    // 告警类型统计
+    const records = await fetchRecentAlarms()
+    const fifteenDaysAgo = new Date(Date.now() - 15 * 24 * 3600 * 1000)
+    const recentAlarms = records.filter(r => !r.createTime || new Date(String(r.createTime).replace(/-/g, '/')) >= fifteenDaysAgo)
+
     const typeMap = {}
     recentAlarms.forEach(r => {
-      const type = r.warningType || '其他'
+      const type = alarmTypeText(r)
       typeMap[type] = (typeMap[type] || 0) + 1
     })
     renderAlarmChart(typeMap)
+
     // 动态信息(最新10条)
     dynamicLogs.value = records.slice(0, 10).map(r => ({
       time: r.createTime || '',
-      event: `${r.warningType || '告警'} - ${r.deviceName || r.deviceCode || ''}`,
+      event: `${alarmTypeText(r)} - ${r.deviceName || r.deviceCode || ''}`,
       alarmStatus: r.alarmStatus ?? 0
     }))
   } catch (e) {
+    console.error(e)
     ElMessage.error('告警数据加载失败')
+    // 加载失败也铺出空态骨架(含统计区间),面板不留白
+    renderAlarmChart({})
   } finally {
     loading.alarm = false
   }
@@ -256,19 +322,30 @@ function renderAlarmChart(typeMap) {
   if (!alarmChartRef.value) return
   if (!alarmChart) alarmChart = echarts.init(alarmChartRef.value)
   const colors = ['#ff3333', '#ffb333', '#33cc33', '#3399ff', '#9966ff', '#ff6699', '#33cccc']
-  const data = Object.entries(typeMap).map(([name, value], i) => ({
+  const data = Object.entries(typeMap || {}).map(([name, value], i) => ({
     name, value, itemStyle: { color: colors[i % colors.length] }
   }))
+  const hasData = data.length > 0
   alarmChart.setOption({
     backgroundColor: 'transparent',
+    // 无告警时保留统计区间与说明,避免面板看起来是空的
+    title: hasData ? { show: false } : {
+      text: '近 15 天暂无告警',
+      subtext: `统计区间 ${alarmRangeText.value}`,
+      left: 'center',
+      top: 'middle',
+      textStyle: { color: '#9ba8c0', fontSize: 12, fontWeight: 'normal' },
+      subtextStyle: { color: '#6f7d95', fontSize: 11 }
+    },
     tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
-    legend: { bottom: 0, textStyle: { color: '#ccc', fontSize: 10 } },
-    series: [{
+    legend: { bottom: 0, textStyle: { color: '#ccc', fontSize: 10 }, show: hasData },
+    // 空数据时不挂饼图系列,否则会残留一个灰色的空圆环
+    series: hasData ? [{
       type: 'pie', radius: ['35%', '60%'], center: ['50%', '40%'],
       label: { color: '#ccc', fontSize: 10 },
-      data: data.length ? data : [{ name: '暂无数据', value: 0 }]
-    }]
-  })
+      data
+    }] : []
+  }, true)
 }
 
 function renderAreaChart() {
@@ -281,47 +358,88 @@ function renderAreaChart() {
     const shortArea = area.length > 6 ? area.slice(0, 6) + '...' : area
     areaMap[shortArea] = (areaMap[shortArea] || 0) + 1
   })
-  const entries = Object.entries(areaMap).sort((a, b) => b[1] - a[1]).slice(0, 8)
+  let entries = Object.entries(areaMap).sort((a, b) => b[1] - a[1]).slice(0, 8)
+  const hasData = entries.length > 0
+  // 无设备时仍按默认区域维度铺出骨架,面板不会空成一片
+  if (!hasData) {
+    entries = DEFAULT_DISTRICTS.map(name => [name, 0])
+  }
   areaChart.setOption({
     backgroundColor: 'transparent',
+    title: hasData ? { show: false } : {
+      text: '暂无设备,按默认区域维度展示',
+      left: 'center',
+      top: 0,
+      textStyle: { color: '#6f7d95', fontSize: 11, fontWeight: 'normal' }
+    },
     tooltip: { trigger: 'axis' },
-    grid: { left: '12%', right: '8%', bottom: '18%', top: '8%' },
+    grid: { left: '12%', right: '8%', bottom: '18%', top: '18%' },
     xAxis: {
       type: 'category', data: entries.map(e => e[0]),
       axisLabel: { color: '#ccc', fontSize: 10, interval: 0, rotate: 30 }
     },
-    yAxis: { type: 'value', axisLabel: { color: '#ccc' }, splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } } },
-    series: [{ type: 'bar', data: entries.map(e => e[1]), itemStyle: { color: '#3399ff' } }]
-  })
+    yAxis: {
+      type: 'value', minInterval: 1,
+      axisLabel: { color: '#ccc' },
+      splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
+    },
+    series: [{ type: 'bar', data: entries.map(e => e[1]), itemStyle: { color: '#3399ff' }, barWidth: '40%' }]
+  }, true)
 }
 
 function renderDeviceTypeChart() {
   if (!deviceTypeChartRef.value) return
   if (!deviceTypeChart) deviceTypeChart = echarts.init(deviceTypeChartRef.value)
-  const typeCount = { '5': 0, '9': 0, '10': 0 }
-  allDevices.value.forEach(d => { if (typeCount[d.typeId] != null) typeCount[d.typeId]++ })
-  const labels = ['供水窨井', '排水窨井', '燃气窨井']
-  const values = [typeCount['5'], typeCount['9'], typeCount['10']]
-  const colors = ['#3399ff', '#67c23a', '#e6a23c']
+
+  // 按台账实际存在的设备类型聚合,避免把不存在的旧类型当成固定三分类
+  const countMap = new Map()
+  allDevices.value.forEach(d => {
+    const label = typeText(d.typeId, d.typeName)
+    countMap.set(label, (countMap.get(label) || 0) + 1)
+  })
+  // 有设备但类型名为空时,至少给出一个占位分类,面板不留白
+  if (!countMap.size && allDevices.value.length) {
+    countMap.set('未分类', allDevices.value.length)
+  }
+
+  const labels = Array.from(countMap.keys())
+  const values = Array.from(countMap.values())
+  const palette = ['#3399ff', '#67c23a', '#e6a23c', '#f56c6c', '#9b59b6']
+
   deviceTypeChart.setOption({
     backgroundColor: 'transparent',
     tooltip: { trigger: 'axis' },
     grid: { left: '12%', right: '8%', bottom: '15%', top: '8%' },
+    title: labels.length ? undefined : {
+      text: '暂无窨井设备',
+      subtext: '设备台账中未查到窨井类设备',
+      left: 'center',
+      top: 'middle',
+      textStyle: { color: '#ccc', fontSize: 12, fontWeight: 'normal' },
+      subtextStyle: { color: '#888', fontSize: 11 }
+    },
     xAxis: {
       type: 'category', data: labels,
       axisLabel: { color: '#ccc', fontSize: 11, interval: 0 }
     },
-    yAxis: { type: 'value', axisLabel: { color: '#ccc' }, splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } } },
-    series: [{
-      type: 'bar', data: values.map((v, i) => ({ value: v, itemStyle: { color: colors[i] } })),
+    yAxis: {
+      type: 'value', minInterval: 1,
+      axisLabel: { color: '#ccc' },
+      splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
+    },
+    series: labels.length ? [{
+      type: 'bar',
+      data: values.map((v, i) => ({ value: v, itemStyle: { color: palette[i % palette.length] } })),
       barWidth: '40%', label: { show: true, position: 'top', color: '#ccc' }
-    }]
-  })
+    }] : []
+  }, true)
 }
 
 // ==================== 百度地图 ====================
 let mapInstance = null
+let mapFitted = false
 const markerMap = new Map()
+/** 兜底中心(沅陵县城),WGS84 口径,与设备台账一致 */
 const YUANLING_CENTER = { lng: 110.393, lat: 28.452 }
 
 function initMap() {
@@ -329,11 +447,30 @@ function initMap() {
   if (!container || typeof BMapGL === 'undefined') return
   try {
     mapInstance = new BMapGL.Map('homeMap')
-    mapInstance.centerAndZoom(new BMapGL.Point(YUANLING_CENTER.lng, YUANLING_CENTER.lat), 13)
+    const [centerLng, centerLat] = convertCoord(YUANLING_CENTER.lng, YUANLING_CENTER.lat)
+    mapInstance.centerAndZoom(new BMapGL.Point(centerLng, centerLat), 15)
     mapInstance.enableScrollWheelZoom(true)
   } catch (e) { console.error('地图初始化失败', e) }
 }
 
+/** 有设备时按设备范围取视野;点太近时限制放大层级,避免只剩一栋楼 */
+function fitMapToDevices(points) {
+  if (!mapInstance || !points.length) return
+  if (points.length === 1) {
+    mapInstance.centerAndZoom(points[0], 16)
+    return
+  }
+  const lngs = points.map(p => p.lng)
+  const lats = points.map(p => p.lat)
+  const span = Math.max(Math.max(...lngs) - Math.min(...lngs), Math.max(...lats) - Math.min(...lats))
+  if (span < 0.002) {
+    const mid = new BMapGL.Point((Math.max(...lngs) + Math.min(...lngs)) / 2, (Math.max(...lats) + Math.min(...lats)) / 2)
+    mapInstance.centerAndZoom(mid, 16)
+  } else {
+    mapInstance.setViewport(points, { margins: [50, 50, 50, 50] })
+  }
+}
+
 function getStatusColor(status) {
   if (status === 'alert') return '#F56C6C'
   if (status === 'offline') return '#909399'
@@ -344,8 +481,13 @@ function refreshMarkers() {
   if (!mapInstance) return
   markerMap.forEach(label => mapInstance.removeOverlay(label))
   markerMap.clear()
+  const devicePoints = []
   allDevices.value.forEach(d => {
     if (d.lng == null || d.lat == null) return
+    // 关键:台账是 WGS84,百度地图是 BD-09,不转换会偏约 1 公里
+    const [bdLng, bdLat] = convertCoord(Number(d.lng), Number(d.lat))
+    const point = new BMapGL.Point(bdLng, bdLat)
+    devicePoints.push(point)
     const color = getStatusColor(d.status)
     const isAttention = d.status === 'alert' || d.status === 'offline'
     const attentionText = d.status === 'alert' ? '告警' : '离线'
@@ -362,13 +504,18 @@ function refreshMarkers() {
           <div><img src="${locationIcon}" alt="" style="width:22px;height:22px;display:block;margin:0 auto;"/></div>
         </div>`
     const label = new BMapGL.Label(html, {
-      position: new BMapGL.Point(d.lng, d.lat),
+      position: point,
       offset: new BMapGL.Size(isAttention ? -62 : -25, isAttention ? -68 : -40)
     })
     label.setStyle({ border: 'none', background: 'transparent', padding: '0', zIndex: isAttention ? '20' : '10' })
     mapInstance.addOverlay(label)
     markerMap.set(d.id, label)
   })
+  // 只在首次拿到设备时校准视野,后续刷新不打断用户手动缩放
+  if (!mapFitted && devicePoints.length) {
+    fitMapToDevices(devicePoints)
+    mapFitted = true
+  }
 }
 
 // ==================== 生命周期 ====================

+ 11 - 3
src/views/subSystem/manholeCover/mRealTimemonit/mRStateMonitoring.vue

@@ -179,6 +179,8 @@ import { Monitor, Location, Refresh } from '@element-plus/icons-vue'
 import { getManholeLayerData, getManholeRelDeviceData } from '@/api/pipeNetwork/basic'
 import { ElMessage } from 'element-plus'
 import locationIcon from '@/assets/images/location.png'
+// 台账存 WGS84,百度地图是 BD-09,不转换会偏约 1 公里
+import { convertCoord } from '@/utils/coordTransform'
 
 // ==================== 数据状态 ====================
 const loading = ref(false)
@@ -252,6 +254,12 @@ let mapInstance = null
 const markerMap = new Map()
 const YUANLING_CENTER = { lng: 110.393, lat: 28.452 }
 
+/** 业务坐标(WGS84)→ 百度坐标点 */
+function toBdPoint(lng, lat) {
+  const [bdLng, bdLat] = convertCoord(Number(lng), Number(lat))
+  return new BMapGL.Point(bdLng, bdLat)
+}
+
 function initMap() {
   const container = document.getElementById('manholeStateMap')
   if (!container || typeof BMapGL === 'undefined') {
@@ -260,7 +268,7 @@ function initMap() {
   }
   try {
     mapInstance = new BMapGL.Map('manholeStateMap')
-    mapInstance.centerAndZoom(new BMapGL.Point(YUANLING_CENTER.lng, YUANLING_CENTER.lat), 13)
+    mapInstance.centerAndZoom(toBdPoint(YUANLING_CENTER.lng, YUANLING_CENTER.lat), 13)
     mapInstance.enableScrollWheelZoom(true)
   } catch (e) {
     console.error('百度地图初始化失败:', e)
@@ -276,7 +284,7 @@ function getStatusColor(status) {
 function addMapMarker(device) {
   if (!mapInstance || device.lng == null || device.lat == null) return
   const color = getStatusColor(device.status)
-  const bPoint = new BMapGL.Point(device.lng, device.lat)
+  const bPoint = toBdPoint(device.lng, device.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);">
       ${device.name}
@@ -307,7 +315,7 @@ async function selectDevice(device) {
   relDevices.value = []
   detailLoading.value = true
   if (mapInstance && device.lng && device.lat) {
-    mapInstance.centerAndZoom(new BMapGL.Point(device.lng, device.lat), 16)
+    mapInstance.centerAndZoom(toBdPoint(device.lng, device.lat), 16)
   }
   highlightMarker(device.id)
   try {

+ 11 - 3
src/views/subSystem/manholeCover/operation/faultMana.vue

@@ -237,6 +237,8 @@ import { ElMessage } from 'element-plus'
 import { getManholeAlarmMonitorList, addManholeWorkOrder, updateManholeDeviceStatus } from '@/api/pipeNetwork/basic'
 import { listWorkOrder } from '@/api/pipeNetwork/ops/workOrder'
 import locationIcon from '@/assets/images/location.png'
+// 台账存 WGS84,百度地图是 BD-09,不转换会偏约 1 公里
+import { convertCoord } from '@/utils/coordTransform'
 
 // ==================== 常量 ====================
 const TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
@@ -281,6 +283,12 @@ let mapInstance = null
 const markerMap = new Map()
 const YUANLING_CENTER = { lng: 110.393, lat: 28.452 }
 
+/** 业务坐标(WGS84)→ 百度坐标点 */
+function toBdPoint(lng, lat) {
+  const [bdLng, bdLat] = convertCoord(Number(lng), Number(lat))
+  return new BMapGL.Point(bdLng, bdLat)
+}
+
 // ==================== 设备数据规范化 ====================
 function normalizeDevice(raw) {
   const md = raw?.manholeData || {}
@@ -434,7 +442,7 @@ const selectDevice = async (device) => {
   detailDrawerVisible.value = true
   highlightMarker(device.id)
   if (mapInstance && device.lng != null && device.lat != null) {
-    mapInstance.centerAndZoom(new BMapGL.Point(device.lng, device.lat), 15)
+    mapInstance.centerAndZoom(toBdPoint(device.lng, device.lat), 15)
   }
   await loadOrderRecords(device)
 }
@@ -535,7 +543,7 @@ function initMap() {
   }
   try {
     mapInstance = new BMapGL.Map('faultManaMap')
-    mapInstance.centerAndZoom(new BMapGL.Point(YUANLING_CENTER.lng, YUANLING_CENTER.lat), 13)
+    mapInstance.centerAndZoom(toBdPoint(YUANLING_CENTER.lng, YUANLING_CENTER.lat), 13)
     mapInstance.enableScrollWheelZoom(true)
   } catch (e) {
     console.error('百度地图初始化失败:', e)
@@ -552,7 +560,7 @@ function getMarkerColor(device) {
 function addMapMarker(device) {
   if (!mapInstance || device.lng == null || device.lat == null) return
   const color = getMarkerColor(device)
-  const bPoint = new BMapGL.Point(device.lng, device.lat)
+  const bPoint = toBdPoint(device.lng, device.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);">
       ${device.name}

+ 111 - 421
src/views/subSystem/pipeNetwork/assetInfo/equipmentBase/index.vue

@@ -1,12 +1,7 @@
 <!-- 物联感知设备管理 -->
 <template>
-  <div class="app-container equipment-page">
-    <!-- 页头 -->
-    <div class="page-head">
-      <div class="page-title">设备统计</div>
-    </div>
-
-    <!-- KPI -->
+  <div class="app-container">
+    <!-- 设备统计 -->
     <div class="kpi-row">
       <el-card shadow="never" class="kpi">
         <div class="kpi-icon total"><el-icon><monitor /></el-icon></div>
@@ -38,111 +33,85 @@
       </el-card>
     </div>
 
-    <!-- 设备台账 -->
-    <div class="page-title section-block-title">设备台账</div>
-
     <!-- 查询条件 -->
-    <el-card shadow="never" class="filter-card">
-      <!-- 设备类型:本子系统下只有几个类型,直接一层平铺,不再分一级/二级 -->
-      <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 equipmentSubtypes"
-            :key="item.id"
-            type="button"
-            class="type-chip"
-            :aria-pressed="queryParams.equipmentTypeId === item.id"
-            @click="selectEquipmentType(item.id)"
-          >{{ item.typeName }}</button>
-        </div>
-      </div>
-      <el-form ref="queryRef" :model="queryParams" :inline="true" class="filters">
-        <el-form-item label="设备编码" prop="equipmentCode">
-          <el-input
-            v-model="queryParams.equipmentCode"
-            placeholder="请输入"
-            clearable
-            class="input--code"
-            @keyup.enter="handleQuery"
-          />
-        </el-form-item>
-        <el-form-item label="设备名称" prop="equipmentName">
-          <el-input
-            v-model="queryParams.equipmentName"
-            placeholder="请输入"
-            clearable
-            class="input--name"
-            @keyup.enter="handleQuery"
-          />
-        </el-form-item>
-        <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>
-          <el-button type="primary" class="btn-query" @click="handleQuery">查询</el-button>
-          <el-button class="btn-reset" @click="resetQuery">重置</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
+    <el-form ref="queryRef" :model="queryParams" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item label="设备编码" prop="equipmentCode">
+        <el-input v-model="queryParams.equipmentCode" placeholder="请输入设备编码" clearable style="width: 200px" @keyup.enter="handleQuery" />
+      </el-form-item>
+      <el-form-item label="设备名称" prop="equipmentName">
+        <el-input v-model="queryParams.equipmentName" placeholder="请输入设备名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
+      </el-form-item>
+      <el-form-item label="设备类型" prop="equipmentTypeId">
+        <el-select v-model="queryParams.equipmentTypeId" placeholder="请选择设备类型" clearable style="width: 200px">
+          <el-option v-for="item in equipmentSubtypes" :key="item.id" :label="item.typeName" :value="item.id" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="运行状态">
+        <el-select v-model="runStatus" placeholder="全部" clearable style="width: 160px">
+          <el-option label="在线" value="online" />
+          <el-option label="离线" value="offline" />
+          <el-option label="报警" value="alarm" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="Plus" @click="handleAdd">新增设备</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
+    </el-row>
 
-    <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
-          v-for="row in equipmentList"
-          :key="row.equipmentId"
-          class="card"
-          :class="{ 'is-selected': selectedId === row.equipmentId, 'is-alarm': getRowAlarm(row.equipmentId) === 1 }"
-          @click="handleView(row)"
-        >
-          <div class="card-top">
-            <div class="card-name">{{ row.equipmentName || "未命名设备" }}</div>
-            <div class="status-group">
-              <span class="badge" :class="getRowOnline(row.equipmentId) === 1 ? 'online' : 'offline'">
-                <span class="badge-dot"></span>
-                <span class="badge-text">{{ getRowOnline(row.equipmentId) === 1 ? "在线" : "离线" }}</span>
-              </span>
-              <span v-if="getRowAlarm(row.equipmentId) === 1" class="badge alarm">
-                <el-icon class="badge-icon"><warning-filled /></el-icon>
-                <span class="badge-text">报警</span>
-              </span>
-            </div>
-          </div>
-
-          <div class="card-code">{{ row.equipmentCode }}</div>
-          <div class="card-district">片区:{{ districtLabel(row.district) }}</div>
-
-          <div class="card-bottom">
-            <span class="type-tag">
-              {{ (row.equipmentTypeId && typeNameMap[row.equipmentTypeId]) || "未分类" }}
-            </span>
-            <div class="card-actions">
-              <el-button link class="link-detail" @click.stop="handleView(row)">详情</el-button>
-              <el-button link class="link-delete" @click.stop="handleDelete(row)">删除</el-button>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <pagination
-        v-show="total > 0"
-        :total="total"
-        v-model:page="queryParams.pageNum"
-        v-model:limit="queryParams.pageSize"
-        @pagination="getList"
-      />
-    </el-card>
+    <!-- 设备台账 -->
+    <el-table v-loading="loading" :data="equipmentList" @row-dblclick="handleView">
+      <el-table-column prop="equipmentCode" label="设备编码" min-width="140" show-overflow-tooltip />
+      <el-table-column prop="equipmentName" label="设备名称" min-width="160" show-overflow-tooltip />
+      <el-table-column label="设备类型" min-width="140" show-overflow-tooltip>
+        <template #default="{ row }">
+          {{ (row.equipmentTypeId && typeNameMap[row.equipmentTypeId]) || "未分类" }}
+        </template>
+      </el-table-column>
+      <el-table-column label="所属片区" width="100" align="center">
+        <template #default="{ row }">{{ districtLabel(row.district) }}</template>
+      </el-table-column>
+      <el-table-column prop="equipmentModel" label="设备型号" min-width="130" show-overflow-tooltip />
+      <el-table-column prop="equipmentLocation" label="安装位置" min-width="200" show-overflow-tooltip />
+      <el-table-column label="运行状态" width="140" align="center">
+        <template #default="{ row }">
+          <el-tag :type="row.onlineStatus === 1 ? 'success' : 'info'" size="small">
+            {{ row.onlineStatus === 1 ? "在线" : "离线" }}
+          </el-tag>
+          <el-tag v-if="row.alarmStatus === 1" type="danger" size="small" style="margin-left: 4px">报警</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="资产状态" width="100" align="center">
+        <template #default="{ row }">
+          <el-tag :type="currentStatusMeta(row.currentStatus).type" size="small">
+            {{ currentStatusMeta(row.currentStatus).text }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="200" align="center" fixed="right" class-name="small-padding fixed-width">
+        <template #default="{ row }">
+          <el-button link type="primary" icon="View" @click="handleView(row)">详情</el-button>
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(row)">修改</el-button>
+          <el-button link type="danger" icon="Delete" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      v-model:page="queryParams.pageNum"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
 
     <!-- 设备详情弹窗 -->
     <el-dialog title="设备详情" v-model="detailOpen" width="800px" append-to-body class="detail-dialog">
@@ -208,15 +177,15 @@
       </template>
       <el-empty v-else description="未选中设备" />
       <template #footer>
-        <div class="dlg-footer">
-          <el-button class="btn-ghost" @click="detailOpen = false">关 闭</el-button>
+        <div class="dialog-footer">
           <el-button type="primary" icon="Edit" @click="handleUpdate(selectedDetail.equipmentInfo)">编辑设备</el-button>
+          <el-button @click="detailOpen = false">关 闭</el-button>
         </div>
       </template>
     </el-dialog>
 
     <!-- 新增/修改弹窗 -->
-    <el-dialog :title="title" v-model="open" width="860px" append-to-body>
+    <el-dialog :title="title" v-model="open" width="860px" append-to-body destroy-on-close>
       <el-form ref="equipmentRef" :model="form" :rules="rules" label-width="110px">
         <div class="form-section-title">基础信息</div>
         <el-row :gutter="16">
@@ -284,12 +253,12 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="资产价值(元)" prop="assetValue">
-              <el-input-number v-model="form.assetValue" :precision="2" :step="100" style="width: 100%" />
+              <el-input-number v-model="form.assetValue" :precision="2" :step="100" controls-position="right" style="width: 100%" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="使用年限(年)" prop="useLife">
-              <el-input-number v-model="form.useLife" :precision="0" :step="1" style="width: 100%" />
+              <el-input-number v-model="form.useLife" :precision="0" :step="1" controls-position="right" style="width: 100%" />
             </el-form-item>
           </el-col>
         </el-row>
@@ -306,7 +275,7 @@
               <div class="coordinate-row">
                 <el-input-number v-model="form.longitude" :precision="6" :step="0.000001" controls-position="right" placeholder="经度" />
                 <el-input-number v-model="form.latitude" :precision="6" :step="0.000001" controls-position="right" placeholder="纬度" />
-                <el-button type="primary" plain @click="openMapPicker">地图选点</el-button>
+                <el-button type="primary" plain icon="Location" @click="openMapPicker">地图选点</el-button>
               </div>
               <div class="coordinate-tip">{{ locationTip || "点击地图获取经纬度,地址将自动回填到安装位置,也可以直接手工修改。" }}</div>
             </el-form-item>
@@ -370,8 +339,10 @@
         解析地址:{{ form.equipmentLocation || "点击地图后自动解析" }}
       </div>
       <template #footer>
-        <el-button @click="mapOpen = false">关 闭</el-button>
-        <el-button type="primary" @click="mapOpen = false">使用当前坐标</el-button>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="mapOpen = false">使用当前坐标</el-button>
+          <el-button @click="mapOpen = false">关 闭</el-button>
+        </div>
       </template>
     </el-dialog>
   </div>
@@ -394,6 +365,8 @@ import {
   updateEquipmentStatus
 } from "@/api/pipeNetwork/pdeviceInform";
 
+defineOptions({ name: "EquipmentBase" });
+
 const { proxy } = getCurrentInstance();
 
 // 数据范围:由系统菜单 query 配置 {"rootTypeId":1} 注入,对应 EquipmentTypeEnum 的 ROOT_ 大类
@@ -403,6 +376,7 @@ const rootTypeId = ref(route.query.rootTypeId ? Number(route.query.rootTypeId) :
 const equipmentList = ref([]);
 const allTypeList = ref([]);
 const loading = ref(false);
+const showSearch = ref(true);
 const open = ref(false);
 const detailOpen = ref(false);
 const mapOpen = ref(false);
@@ -414,7 +388,6 @@ const mapPoints = ref([]);
 const total = ref(0);
 const title = ref("");
 const selectedDetail = ref({});
-const selectedId = ref("");
 const locationTip = ref("");
 
 const statistics = reactive({
@@ -455,14 +428,6 @@ function getRowStatus(equipmentId) {
   return equipmentList.value.find(item => item.equipmentId === equipmentId)?.currentStatus;
 }
 
-function getRowOnline(equipmentId) {
-  return equipmentList.value.find(item => item.equipmentId === equipmentId)?.onlineStatus;
-}
-
-function getRowAlarm(equipmentId) {
-  return equipmentList.value.find(item => item.equipmentId === equipmentId)?.alarmStatus;
-}
-
 // parentId 为 0(或空)表示父类
 function isTopLevelType(item) {
   const parentId = item?.parentId;
@@ -530,7 +495,7 @@ const equipmentTypeTree = computed(() => {
   return branch ? [branch] : tree;
 });
 
-// 复用真实类型树;若存在更深层级,将可选叶子平铺在筛选行。
+// 复用真实类型树;若存在更深层级,将可选叶子平铺在类型下拉里。
 function collectSelectableTypes(nodes) {
   return nodes.flatMap((node) => node.children?.length
     ? collectSelectableTypes(node.children)
@@ -541,15 +506,10 @@ function collectSelectableTypes(nodes) {
  * 可选的设备类型
  *
  * equipmentTypeTree 已按菜单注入的 rootTypeId 收窄到本子系统(排水只有 6 个叶子类型),
- * 数量不多,直接一层平铺成筛选标签,不再做一级/二级两级联动。
+ * 数量不多,直接平铺成下拉选项。
  */
 const equipmentSubtypes = computed(() => collectSelectableTypes(equipmentTypeTree.value));
 
-function selectEquipmentType(id) {
-  queryParams.value.equipmentTypeId = id;
-  handleQuery();
-}
-
 function currentStatusMeta(value) {
   const map = {
     0: { text: "在用", type: "success" },
@@ -761,7 +721,6 @@ function handleView(row) {
   if (!row?.equipmentId) {
     return;
   }
-  selectedId.value = row.equipmentId;
   return getEquipmentDetail(row.equipmentId).then((response) => {
     selectedDetail.value = response.data || {};
     detailOpen.value = true;
@@ -779,7 +738,6 @@ function handleDelete(row) {
       proxy.$modal.msgSuccess("删除成功");
       if (selectedDetail.value?.equipmentInfo?.equipmentId === row.equipmentId) {
         selectedDetail.value = {};
-        selectedId.value = "";
         detailOpen.value = false;
       }
       return Promise.all([getList(), loadAssistData(), loadStatistics()]);
@@ -823,34 +781,11 @@ onMounted(() => {
 </script>
 
 <style scoped>
-.equipment-page {
-  background: #f9f9f9;
-}
-
-/* 页头 */
-.page-head {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 20px;
-}
-
-.page-title {
-  font-size: 20px;
-  font-weight: 600;
-  color: #0a1628;
-  line-height: 1.4;
-}
-
-.btn-primary :deep(.el-icon) {
-  font-size: 14px;
-}
-
 /* KPI */
 .kpi-row {
   display: flex;
   gap: 16px;
-  margin-bottom: 20px;
+  margin-bottom: 16px;
 }
 
 .kpi {
@@ -911,189 +846,38 @@ onMounted(() => {
   color: #0a1628;
 }
 
-/* 台账卡片 */
-.filter-card {
-  margin-bottom: 20px;
-  border-radius: 12px;
-}
-
-.section-block-title {
-  margin: 20px 0 12px;
-}
-
-.ledger {
-  border-radius: 12px;
-}
-
-.ledger-toolbar {
+/* 详情弹窗 */
+.dlg-summary {
   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;
-  gap: 14px;
-  padding: 20px;
-}
-
-/* 筛选栏 */
-.type-filter {
-  display: flex;
-  align-items: flex-start;
-  gap: 12px;
-  margin-bottom: 16px;
-  padding-bottom: 16px;
-  border-bottom: 1px solid #f0f1f3;
-}
-
-.filter-label {
-  flex: none;
-  padding-top: 6px;
-  color: #6b7280;
-  font-size: 12px;
-  line-height: 18px;
-}
-
-.type-filter-options {
-  display: flex;
   flex-wrap: wrap;
-  gap: 8px;
-}
-
-.type-chip {
-  padding: 0 14px;
-  height: 30px;
-  border: 1px solid #e5e6eb;
-  border-radius: 999px;
-  background: #fff;
-  color: #4e5969;
-  font: inherit;
-  font-size: 13px;
-  white-space: nowrap;
-  cursor: pointer;
-  transition: background .15s, color .15s, border-color .15s;
-}
-
-.type-chip:hover {
-  color: var(--el-color-primary);
-  border-color: var(--el-color-primary-light-5);
+  padding: 14px;
+  border-radius: 8px;
+  background: #f5f9ff;
 }
 
-.type-chip[aria-pressed="true"] {
-  color: var(--el-color-primary);
-  border-color: var(--el-color-primary);
-  background: var(--el-color-primary-light-9);
+.summary-name {
+  font-size: 15px;
   font-weight: 500;
+  color: #014db2;
 }
 
-.type-chip:focus-visible {
-  outline: 2px solid var(--el-color-primary);
-  outline-offset: 2px;
-}
-
-.filters {
-  display: flex;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 4px 16px;
-}
-
-.filters :deep(.el-form-item) {
-  margin-right: 0;
-  margin-bottom: 0;
-}
-
-.filters :deep(.el-form-item__label) {
+.summary-code {
   font-size: 12px;
   font-weight: 400;
   color: #6b7280;
-  padding-right: 8px;
-}
-
-.input--code,
-.input--name {
-  width: 150px;
 }
 
-.select--status {
-  width: 120px;
-}
-
-.btn-query {
-  height: 34px;
-  padding: 0 16px;
-  border-radius: 8px;
-  font-size: 13px;
-  font-weight: 500;
-}
-
-.btn-reset {
-  height: 34px;
-  padding: 0 16px;
-  border-radius: 8px;
-  font-size: 13px;
-  font-weight: 400;
-}
-
-/* 设备卡片 */
-.grid {
-  display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
-  align-items: start;
-  gap: 10px;
-  min-height: 160px;
-}
-
-.card {
-  display: flex;
-  flex-direction: column;
-  gap: 6px;
-  align-self: start;
-  min-height: 0;
-  padding: 10px 12px;
-  border-radius: 10px;
-  background: #fff;
-  border: 1px solid #ebebed;
-  cursor: pointer;
-  transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
-}
-
-.card:hover {
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
-}
-
-.card.is-selected {
-  background: #f5f9ff;
-  border: 1.5px solid #014db2;
-}
-
-.card.is-alarm {
-  border: 1.5px solid #fca5a5;
-}
-
-.card-top {
-  display: flex;
+.summary-tag {
+  display: inline-flex;
   align-items: center;
-  justify-content: space-between;
-  gap: 8px;
-}
-
-.card-name {
-  min-width: 0;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  font-size: 14px;
+  padding: 3px 8px;
+  border-radius: 4px;
+  background: #fff;
+  font-size: 11px;
   font-weight: 500;
-  color: #0a1628;
+  color: #014db2;
 }
 
 .status-group {
@@ -1146,93 +930,6 @@ onMounted(() => {
   color: #ef4444;
 }
 
-.card-code {
-  font-size: 12px;
-  font-weight: 400;
-  color: #9ca3af;
-}
-
-.card-district {
-  overflow: hidden;
-  color: #6b7280;
-  font-size: 12px;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.card-bottom {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  gap: 8px;
-}
-
-.type-tag {
-  display: inline-flex;
-  align-items: center;
-  padding: 3px 8px;
-  border-radius: 4px;
-  background: #edf3fc;
-  font-size: 11px;
-  font-weight: 500;
-  color: #014db2;
-}
-
-.card-actions {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-
-.card-actions :deep(.el-button) {
-  height: auto;
-  padding: 0;
-  font-size: 12px;
-  font-weight: 500;
-}
-
-.link-detail {
-  color: #014db2;
-}
-
-.link-delete {
-  color: #ef4444;
-}
-
-/* 详情弹窗 */
-.dlg-summary {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  flex-wrap: wrap;
-  padding: 14px;
-  border-radius: 8px;
-  background: #f5f9ff;
-}
-
-.summary-name {
-  font-size: 15px;
-  font-weight: 500;
-  color: #014db2;
-}
-
-.summary-code {
-  font-size: 12px;
-  font-weight: 400;
-  color: #6b7280;
-}
-
-.summary-tag {
-  display: inline-flex;
-  align-items: center;
-  padding: 3px 8px;
-  border-radius: 4px;
-  background: #fff;
-  font-size: 11px;
-  font-weight: 500;
-  color: #014db2;
-}
-
 .dlg-grid {
   margin-top: 16px;
 }
@@ -1299,7 +996,6 @@ onMounted(() => {
   font-size: 13px;
 }
 
-.dlg-footer,
 .dialog-footer {
   display: flex;
   align-items: center;
@@ -1321,11 +1017,5 @@ onMounted(() => {
   .kpi {
     flex: 1 1 100%;
   }
-
-  .page-head {
-    flex-direction: column;
-    align-items: flex-start;
-    gap: 12px;
-  }
 }
 </style>

+ 82 - 82
src/views/subSystem/pipeNetwork/assetInfo/monitorPoint/index.vue

@@ -1,13 +1,5 @@
 <template>
-  <div class="app-container monitor-point-page">
-    <div class="page-head">
-      <div>
-        <div class="page-title">关键监测点位</div>
-        <div class="page-subtitle">统一维护点位档案与设备关联,让每一份监测数据都有明确归属。</div>
-      </div>
-      <el-button type="primary" icon="Plus" @click="openCreate">新建监测点</el-button>
-    </div>
-
+  <div class="app-container">
     <div class="stats-grid">
       <el-card v-for="item in statCards" :key="item.label" shadow="never" class="stat-card" :class="item.type">
         <div class="stat-label">{{ item.label }}</div>
@@ -15,54 +7,56 @@
       </el-card>
     </div>
 
-    <el-card shadow="never" class="list-panel">
-      <template #header>
-        <div class="panel-header"><span>监测点列表</span><span class="panel-tip">共 {{ total }} 条</span></div>
-      </template>
-      <el-form ref="queryRef" :model="query" :inline="true" class="filter-form" @submit.prevent>
-        <el-form-item label="监测点编码" prop="pointCode">
-          <el-input v-model="query.pointCode" clearable placeholder="输入监测点编码" @keyup.enter="search" />
-        </el-form-item>
-        <el-form-item label="监测点名称" prop="pointName">
-          <el-input v-model="query.pointName" clearable placeholder="输入监测点名称" @keyup.enter="search" />
-        </el-form-item>
-        <el-form-item label="关联状态" prop="associationStatus">
-          <el-select v-model="query.associationStatus" clearable placeholder="全部状态">
-            <el-option label="已关联" value="linked" />
-            <el-option label="待关联" value="pending" />
-          </el-select>
-        </el-form-item>
-        <el-form-item class="filter-actions">
-          <el-button type="primary" icon="Search" @click="search">查询</el-button>
-          <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-        </el-form-item>
-      </el-form>
+    <el-form ref="queryRef" :model="query" :inline="true" v-show="showSearch" label-width="96px" @submit.prevent>
+      <el-form-item label="监测点编码" prop="pointCode">
+        <el-input v-model="query.pointCode" clearable placeholder="请输入监测点编码" style="width: 200px" @keyup.enter="search" />
+      </el-form-item>
+      <el-form-item label="监测点名称" prop="pointName">
+        <el-input v-model="query.pointName" clearable placeholder="请输入监测点名称" style="width: 200px" @keyup.enter="search" />
+      </el-form-item>
+      <el-form-item label="关联状态" prop="associationStatus">
+        <el-select v-model="query.associationStatus" clearable placeholder="全部状态" style="width: 160px">
+          <el-option label="已关联" value="linked" />
+          <el-option label="待关联" value="pending" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="search">搜索</el-button>
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
 
-      <el-table v-loading="loading" :data="pointList" row-key="pointId" class="point-table">
-        <el-table-column label="监测点编码" prop="pointCode" min-width="150" />
-        <el-table-column label="监测点名称" prop="pointName" min-width="180" show-overflow-tooltip />
-        <el-table-column v-if="!rootTypeId" label="监测点类型" min-width="130">
-          <template #default="{ row }">{{ pointTypeLabel(row.pointType) }}</template>
-        </el-table-column>
-        <el-table-column label="位置" prop="location" min-width="240" show-overflow-tooltip />
-        <el-table-column label="关联设备" width="120">
-          <template #default="{ row }">
-            <span v-if="deviceCount(row)" class="linked-count">{{ deviceCount(row) }} 台</span>
-            <span v-else class="pending-count">待关联</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" fixed="right" width="320" align="center">
-          <template #default="{ row }">
-            <el-button link type="primary" icon="View" @click="openDetail(row)">详情</el-button>
-            <el-button link type="primary" icon="Edit" @click="openEdit(row)">编辑</el-button>
-            <el-button link type="primary" icon="Connection" @click="openAssociation(row)">关联设备</el-button>
-            <el-button link type="danger" icon="Delete" @click="removePoint(row)">删除</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <el-empty v-if="!loading && !pointList.length" description="未找到匹配的监测点" />
-      <pagination v-show="total > 0" :total="total" v-model:page="query.pageNum" v-model:limit="query.pageSize" @pagination="loadPoints" />
-    </el-card>
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="Plus" @click="openCreate">新建监测点</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="loadPoints" />
+    </el-row>
+
+    <el-table v-loading="loading" :data="pointList" row-key="pointId">
+      <el-table-column label="监测点编码" prop="pointCode" min-width="150" show-overflow-tooltip />
+      <el-table-column label="监测点名称" prop="pointName" min-width="180" show-overflow-tooltip />
+      <el-table-column v-if="!rootTypeId" label="监测点类型" min-width="130">
+        <template #default="{ row }">{{ pointTypeLabel(row.pointType) }}</template>
+      </el-table-column>
+      <el-table-column label="位置" prop="location" min-width="240" show-overflow-tooltip />
+      <el-table-column label="关联设备" width="120" align="center">
+        <template #default="{ row }">
+          <span v-if="deviceCount(row)" class="linked-count">{{ deviceCount(row) }} 台</span>
+          <span v-else class="pending-count">待关联</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" fixed="right" width="300" align="center" class-name="small-padding fixed-width">
+        <template #default="{ row }">
+          <el-button link type="primary" icon="View" @click="openDetail(row)">详情</el-button>
+          <el-button link type="primary" icon="Edit" @click="openEdit(row)">编辑</el-button>
+          <el-button link type="primary" icon="Connection" @click="openAssociation(row)">关联设备</el-button>
+          <el-button link type="danger" icon="Delete" @click="removePoint(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total > 0" :total="total" v-model:page="query.pageNum" v-model:limit="query.pageSize" @pagination="loadPoints" />
 
     <el-dialog v-model="detailVisible" title="监测点详情" width="860px" append-to-body>
       <template v-if="detail.pointInfo">
@@ -78,7 +72,7 @@
           <el-descriptions-item label="备注">{{ detail.pointInfo.remark || "-" }}</el-descriptions-item>
         </el-descriptions>
         <div class="association-header"><div><div class="section-title">关联设备 {{ detailEquipment.length }}</div><div class="section-tip">设备仅可归属一个监测点,解除关联不会删除历史数据。</div></div><el-button type="primary" icon="Connection" @click="openAssociation(detail.pointInfo)">关联设备</el-button></div>
-        <el-table :data="detailEquipment" border size="small">
+        <el-table :data="detailEquipment">
           <el-table-column label="设备编码" prop="equipmentCode" min-width="140" />
           <el-table-column label="设备名称" prop="equipmentName" min-width="180" show-overflow-tooltip />
           <el-table-column label="设备类型" prop="equipmentTypeName" min-width="130" />
@@ -102,27 +96,43 @@
         <el-form-item label="地图选点"><el-button type="primary" plain icon="Location" @click="openMapPicker">在地图上选择位置</el-button><span class="coordinate-tip">{{ locationTip || "点击地图后自动回填经纬度和地址" }}</span></el-form-item>
         <el-form-item label="备注" prop="remark"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit/></el-form-item>
       </el-form>
-      <template #footer><el-button @click="formVisible = false">取消</el-button><el-button type="primary" @click="savePoint">保存</el-button></template>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="savePoint">保 存</el-button>
+          <el-button @click="formVisible = false">取 消</el-button>
+        </div>
+      </template>
     </el-dialog>
 
     <el-dialog v-model="mapVisible" title="地图选点" width="760px" append-to-body>
       <div class="map-picker"><GisMapCanvas :center="mapCenter" :points="mapPoints" :enable-clustering="false" :show-marker-name="false" @map-click="handleMapClick" /></div>
       <div class="map-coordinates">当前坐标:经度 {{ form.longitude ?? "-" }},纬度 {{ form.latitude ?? "-" }}</div>
       <div class="map-coordinates">解析地址:{{ form.location || "点击地图后自动解析" }}</div>
-      <template #footer><el-button @click="mapVisible = false">关闭</el-button><el-button type="primary" @click="mapVisible = false">使用当前坐标</el-button></template>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="mapVisible = false">使用当前坐标</el-button>
+          <el-button @click="mapVisible = false">关 闭</el-button>
+        </div>
+      </template>
     </el-dialog>
 
     <el-dialog v-model="associationVisible" title="关联监测设备" width="900px" append-to-body>
       <div class="association-target">当前点位:{{ associationPoint.pointName || "-" }} / {{ associationPoint.pointCode || "-" }}<br />已关联 {{ associationEquipment.length }} 台设备;本次仅添加新关联。</div>
       <el-input v-model="equipmentKeyword" clearable prefix-icon="Search" placeholder="搜索设备名称或编码" class="equipment-search" />
-      <el-table :data="availableEquipment" border size="small" max-height="380" @selection-change="selectedEquipment = $event">
-        <el-table-column type="selection" width="50" />
-        <el-table-column label="设备编码" prop="equipmentCode" min-width="140" />
+      <el-table :data="availableEquipment" max-height="380" @selection-change="selectedEquipment = $event">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column label="设备编码" prop="equipmentCode" min-width="140" show-overflow-tooltip />
         <el-table-column label="设备名称" prop="equipmentName" min-width="200" show-overflow-tooltip />
         <el-table-column label="设备类型" prop="equipmentTypeName" min-width="140" />
         <el-table-column label="位置" prop="equipmentLocation" min-width="180" show-overflow-tooltip />
       </el-table>
-      <template #footer><span class="association-summary">本次新增 {{ selectedEquipment.length }} 台</span><el-button @click="associationVisible = false">取消</el-button><el-button type="primary" @click="bind">确认关联</el-button></template>
+      <template #footer>
+        <div class="dialog-footer">
+          <span class="association-summary">本次新增 {{ selectedEquipment.length }} 台</span>
+          <el-button type="primary" @click="bind">确认关联</el-button>
+          <el-button @click="associationVisible = false">取 消</el-button>
+        </div>
+      </template>
     </el-dialog>
   </div>
 </template>
@@ -142,6 +152,7 @@ const rootTypeId = computed(() => {
 });
 const pointTypeOptions = [{ value: 1, label: "供水监测点" }, { value: 2, label: "排水监测点" }, { value: 3, label: "燃气监测点" }];
 const loading = ref(false);
+const showSearch = ref(true);
 const pointList = ref([]);
 const total = ref(0);
 const queryRef = ref();
@@ -194,41 +205,30 @@ onMounted(() => { Promise.all([loadPoints(), loadStatistics()]); });
 </script>
 
 <style scoped>
-.monitor-point-page { background: #f5f8fc; min-height: 100%; }
-.page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
-.page-title { color: #172b4d; font-size: 26px; font-weight: 700; line-height: 1.3; }
-.page-subtitle, .panel-tip, .section-tip, .coordinate-tip { color: #68778d; font-size: 13px; }
-.page-subtitle { margin-top: 8px; }
-.stats-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; margin-bottom: 18px; }
-.stat-card { border: 1px solid #edf0f5; border-radius: 8px; }
+.stats-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; margin-bottom: 16px; }
+.stat-card { border-radius: 8px; }
 .stat-card :deep(.el-card__body) { padding: 20px 22px; }
 .stat-label { color: #68778d; font-size: 13px; }
 .stat-value { margin-top: 10px; color: #172b4d; font-size: 28px; font-weight: 700; }
 .stat-card.warning .stat-value { color: #b66a13; }
 .stat-card.info .stat-value { color: #1a73e8; }
-.list-panel { border: 1px solid #edf0f5; border-radius: 8px; }
-.panel-header, .association-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
-.panel-header { font-weight: 700; }
-.filter-form { padding-top: 4px; border-bottom: 1px solid #eef1f5; }
-.filter-form .el-input { width: 220px; }
-.filter-form .el-select { width: 180px; }
-.filter-actions { margin-left: auto; }
-.point-table { margin-top: 18px; }
 .linked-count { color: #1a73e8; font-weight: 600; }
 .pending-count { color: #b66a13; font-weight: 600; }
 .detail-hero { display: flex; justify-content: space-between; align-items: center; padding: 6px 0 18px; }
 .detail-name { color: #172b4d; font-size: 20px; font-weight: 700; }
 .detail-code { margin-top: 5px; color: #68778d; }
 .detail-info { margin-bottom: 24px; }
-.association-header { margin-bottom: 14px; }
+.association-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 14px; }
 .section-title { color: #172b4d; font-size: 16px; font-weight: 700; }
+.section-tip, .coordinate-tip { color: #68778d; font-size: 13px; }
 .section-tip { margin-top: 5px; }
 .map-picker { height: 420px; overflow: hidden; border-radius: 6px; }
 .map-coordinates { margin-top: 10px; color: #526176; font-size: 13px; }
 .coordinate-tip { margin-left: 12px; }
 .association-target { margin-bottom: 14px; padding: 12px 14px; color: #315a89; line-height: 1.7; background: #eff6ff; border-radius: 6px; }
 .equipment-search { width: 360px; margin-bottom: 14px; }
-.association-summary { float: left; color: #68778d; line-height: 32px; }
-@media (max-width: 900px) { .stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .filter-actions { margin-left: 0; } }
-@media (max-width: 600px) { .page-head { display: block; } .page-head .el-button { margin-top: 14px; } .stats-grid { gap: 10px; } .filter-form .el-form-item, .filter-form .el-input, .filter-form .el-select { width: 100%; } .equipment-search { width: 100%; } }
+.association-summary { margin-right: auto; color: #68778d; }
+.dialog-footer { display: flex; align-items: center; justify-content: flex-end; gap: 10px; }
+@media (max-width: 900px) { .stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
+@media (max-width: 600px) { .stats-grid { gap: 10px; } .equipment-search { width: 100%; } }
 </style>

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott