Bladeren bron

feat:燃气管网-基础数据管理

null 1 maand geleden
bovenliggende
commit
ebe576ec6a

+ 39 - 0
src/api/pipeNetwork/basic.js

@@ -67,6 +67,9 @@ export function deleteRepairRecord(id) {
 export function getEquipmentPage(pageNum, pageSize, params) {
   return request({ url: '/EquipmentBase/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
 }
+export function getEquipmentFullPage(pageNum, pageSize, params) {
+  return request({ url: '/api/equipment/findFullByPage', method: 'get', params: { pageNum, pageSize, ...params } })
+}
 export function getEquipmentById(id) {
   return request({ url: '/EquipmentBase/getById/' + id, method: 'get' })
 }
@@ -88,6 +91,19 @@ export function getEquipmentAbnormalList() {
 export function getPointRelByEquipment(equipmentId) {
   return request({ url: '/api/equipment/point-rel/' + equipmentId, method: 'get' })
 }
+// ============ 管点-设备关联 ============
+export function getPipePointDevices(pointId) {
+  return request({ url: '/api/gas-pipe-point/device/list/' + pointId, method: 'get' })
+}
+export function addPipePointDeviceRel(data) {
+  return request({ url: '/api/gas-pipe-point/device/save', method: 'post', data })
+}
+export function addPipePointDeviceRelBatch(dataList) {
+  return request({ url: '/api/gas-pipe-point/device/save-batch', method: 'post', dataList })
+}
+export function deletePipePointDeviceRel(relId) {
+  return request({ url: '/api/gas-pipe-point/device/delete', method: 'delete', params: { relId } })
+}
 export function savePointRel(data) {
   return request({ url: '/api/equipment/point-rel/save', method: 'post', data })
 }
@@ -112,6 +128,29 @@ export function deletePoint(id) {
   return request({ url: '/api/monitor-point/deleteById', method: 'delete', params: { id } })
 }
 
+// ============ 燃气管点管理 ============
+export function getGasPipePointPage(pageNum, pageSize, params) {
+  return request({ url: '/api/gas-pipe-point/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
+}
+export function getGasPipePointById(id) {
+  return request({ url: '/api/gas-pipe-point/getById/' + id, method: 'get' })
+}
+export function getGasPipePointByNetwork(networkId) {
+  return request({ url: '/api/gas-pipe-point/list-by-network/' + networkId, method: 'get' })
+}
+export function getGasPipePointOptions() {
+  return request({ url: '/api/gas-pipe-point/options', method: 'get' })
+}
+export function addGasPipePoint(data) {
+  return request({ url: '/api/gas-pipe-point/save', method: 'post', data })
+}
+export function updateGasPipePoint(data) {
+  return request({ url: '/api/gas-pipe-point/updateById', method: 'put', data })
+}
+export function deleteGasPipePoint(id) {
+  return request({ url: '/api/gas-pipe-point/deleteById', method: 'delete', params: { id } })
+}
+
 // ============ GIS一张图 ============
 export function getGisPipeLines(params) {
   return request({ url: '/api/gis/pipe-lines', method: 'get', params })

+ 147 - 60
src/views/subSystem/basic/Equipment.vue

@@ -1,15 +1,18 @@
 <template>
   <div class="equipment-container">
     <el-card class="search-card">
-      <el-form :inline="true" :model="searchForm">
+      <el-form :inline="true" :model="searchForm" class="search-form">
         <el-form-item label="设备编码">
-          <el-input v-model="searchForm.equipmentCode" placeholder="请输入设备编码" clearable />
+          <el-input v-model="searchForm.equipmentCode" placeholder="请输入设备编码" clearable style="width:180px" />
         </el-form-item>
         <el-form-item label="设备名称">
-          <el-input v-model="searchForm.equipmentName" placeholder="请输入设备名称" clearable />
+          <el-input v-model="searchForm.equipmentName" placeholder="请输入设备名称" clearable style="width:180px" />
         </el-form-item>
         <el-form-item label="设备型号">
-          <el-input v-model="searchForm.equipmentModel" placeholder="请输入设备型号" clearable />
+          <el-input v-model="searchForm.equipmentModel" placeholder="请输入设备型号" clearable style="width:160px" />
+        </el-form-item>
+        <el-form-item label="制造商">
+          <el-input v-model="searchForm.manufacturer" placeholder="请输入制造商" clearable style="width:160px" />
         </el-form-item>
         <el-form-item>
           <el-button type="primary" @click="handleSearch">查询</el-button>
@@ -20,20 +23,48 @@
     </el-card>
 
     <el-card class="table-card">
-      <el-table :data="tableData" border stripe v-loading="loading">
-        <el-table-column prop="equipmentCode" label="设备编码" min-width="120" />
-        <el-table-column prop="equipmentName" label="设备名称" min-width="150" />
-        <el-table-column prop="equipmentModel" label="设备型号" width="120" />
-        <el-table-column prop="equipmentSpec" label="规格参数" width="120" />
+      <el-table :data="tableData" border stripe v-loading="loading" style="width: 100%">
+        <el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
+        <el-table-column prop="equipmentName" label="设备名称" min-width="160" />
+        <el-table-column prop="equipmentModel" label="设备型号" width="110" />
         <el-table-column prop="manufacturer" label="制造商" width="110" />
-        <el-table-column prop="equipmentLocation" label="安装位置" min-width="150" />
-        <el-table-column prop="maintainer" label="维护人员" width="100" />
-        <el-table-column label="运行状态" width="100">
+        <el-table-column prop="networkName" label="所属管线" min-width="150">
+          <template #default="{ row }">
+            <span v-if="row.networkName">{{ row.networkName }}</span>
+            <el-tag v-else type="info" size="small">未关联</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="pointName" label="所属管点" min-width="150">
+          <template #default="{ row }">
+            <span v-if="row.pointName">{{ row.pointName }}</span>
+            <el-tag v-else type="info" size="small">未关联</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="运行状态" width="90">
           <template #default="{ row }">
-            <el-tag :type="row.statusInfo ? 'success' : 'warning'" size="small">{{ row.statusInfo ? '在线' : '未知' }}</el-tag>
+            <el-tag v-if="row.currentStatus === 1" type="success" size="small">在用</el-tag>
+            <el-tag v-else-if="row.currentStatus === 2" type="warning" size="small">闲置</el-tag>
+            <el-tag v-else-if="row.currentStatus === 3" type="danger" size="small">维修</el-tag>
+            <el-tag v-else-if="row.currentStatus === 4" type="info" size="small">报废</el-tag>
+            <el-tag v-else-if="row.currentStatus === 5" type="warning" size="small">待入库</el-tag>
+            <el-tag v-else type="info" size="small">未知</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="200" fixed="right">
+        <el-table-column label="报警" width="70">
+          <template #default="{ row }">
+            <el-tag :type="row.alarmStatus === 1 ? 'danger' : 'success'" size="small">
+              {{ row.alarmStatus === 1 ? '报警' : '正常' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="在线" width="70">
+          <template #default="{ row }">
+            <el-tag :type="row.onlineStatus === 1 ? 'success' : 'info'" size="small">
+              {{ row.onlineStatus === 1 ? '在线' : '离线' }}
+            </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="info" size="small" @click="handleDetail(row)">详情</el-button>
@@ -48,51 +79,106 @@
       />
     </el-card>
 
-    <el-dialog title="设备详情" v-model="detailVisible" width="700px">
+    <el-dialog title="设备详情" v-model="detailVisible" width="750px">
       <el-descriptions v-if="detailData" :column="2" border size="small">
-        <el-descriptions-item label="设备编码">{{ detailData.equipmentCode }}</el-descriptions-item>
-        <el-descriptions-item label="设备名称">{{ detailData.equipmentName }}</el-descriptions-item>
+        <el-descriptions-item label="设备编码" :span="1">{{ detailData.equipmentCode }}</el-descriptions-item>
+        <el-descriptions-item label="设备名称" :span="1">{{ detailData.equipmentName }}</el-descriptions-item>
         <el-descriptions-item label="设备型号">{{ detailData.equipmentModel }}</el-descriptions-item>
+        <el-descriptions-item label="规格参数">{{ detailData.equipmentSpec }}</el-descriptions-item>
         <el-descriptions-item label="制造商">{{ detailData.manufacturer }}</el-descriptions-item>
+        <el-descriptions-item label="生产日期">{{ detailData.productionDate }}</el-descriptions-item>
+        <el-descriptions-item label="所属管线">{{ detailData.networkName || '未关联' }}</el-descriptions-item>
+        <el-descriptions-item label="管线等级">{{ detailData.networkLevel || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="所属管点">{{ detailData.pointName || '未关联' }}</el-descriptions-item>
+        <el-descriptions-item label="管点类型">{{ detailData.pointType || '-' }}</el-descriptions-item>
         <el-descriptions-item label="安装位置">{{ detailData.equipmentLocation }}</el-descriptions-item>
-        <el-descriptions-item label="维护人员">{{ detailData.maintainer }}</el-descriptions-item>
-        <el-descriptions-item label="联系方式">{{ detailData.maintainerPhone }}</el-descriptions-item>
         <el-descriptions-item label="安装时间">{{ detailData.installTime }}</el-descriptions-item>
+        <el-descriptions-item label="运行状态">
+          <el-tag v-if="detailData.currentStatus === 1" type="success" size="small">在用</el-tag>
+          <el-tag v-else-if="detailData.currentStatus === 2" type="warning" size="small">闲置</el-tag>
+          <el-tag v-else-if="detailData.currentStatus === 3" type="danger" size="small">维修</el-tag>
+          <el-tag v-else-if="detailData.currentStatus === 4" type="info" size="small">报废</el-tag>
+          <el-tag v-else-if="detailData.currentStatus === 5" type="warning" size="small">待入库</el-tag>
+          <span v-else>-</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="报警状态">
+          <el-tag :type="detailData.alarmStatus === 1 ? 'danger' : 'success'" size="small">
+            {{ detailData.alarmStatus === 1 ? '报警' : '正常' }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="在线状态">
+          <el-tag :type="detailData.onlineStatus === 1 ? 'success' : 'info'" size="small">
+            {{ detailData.onlineStatus === 1 ? '在线' : '离线' }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="维护人员">{{ detailData.maintainer || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="联系方式">{{ detailData.maintainerPhone || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</el-descriptions-item>
       </el-descriptions>
     </el-dialog>
 
     <el-dialog :title="dialogTitle" v-model="dialogVisible" width="650px" destroy-on-close>
       <el-form :model="formData" ref="formRef" label-width="100px">
-        <el-form-item label="设备编码" prop="equipmentCode">
-          <el-input v-model="formData.equipmentCode" placeholder="请输入设备编码" />
-        </el-form-item>
-        <el-form-item label="设备名称" prop="equipmentName">
-          <el-input v-model="formData.equipmentName" placeholder="请输入设备名称" />
-        </el-form-item>
-        <el-form-item label="设备型号">
-          <el-input v-model="formData.equipmentModel" />
-        </el-form-item>
-        <el-form-item label="规格参数">
-          <el-input v-model="formData.equipmentSpec" />
-        </el-form-item>
-        <el-form-item label="制造商">
-          <el-input v-model="formData.manufacturer" />
-        </el-form-item>
-        <el-form-item label="安装位置">
-          <el-input v-model="formData.equipmentLocation" />
-        </el-form-item>
-        <el-form-item label="经度">
-          <el-input-number v-model="formData.longitude" :precision="10" style="width: 100%" />
-        </el-form-item>
-        <el-form-item label="纬度">
-          <el-input-number v-model="formData.latitude" :precision="10" style="width: 100%" />
-        </el-form-item>
-        <el-form-item label="维护人员">
-          <el-input v-model="formData.maintainer" />
-        </el-form-item>
-        <el-form-item label="联系方式">
-          <el-input v-model="formData.maintainerPhone" />
-        </el-form-item>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="设备编码" prop="equipmentCode">
+              <el-input v-model="formData.equipmentCode" placeholder="请输入设备编码" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="设备名称" prop="equipmentName">
+              <el-input v-model="formData.equipmentName" placeholder="请输入设备名称" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="设备型号">
+              <el-input v-model="formData.equipmentModel" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="规格参数">
+              <el-input v-model="formData.equipmentSpec" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="制造商">
+              <el-input v-model="formData.manufacturer" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="安装位置">
+              <el-input v-model="formData.equipmentLocation" />
+            </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" 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" style="width:100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="维护人员">
+              <el-input v-model="formData.maintainer" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="联系方式">
+              <el-input v-model="formData.maintainerPhone" />
+            </el-form-item>
+          </el-col>
+        </el-row>
         <el-form-item label="备注">
           <el-input v-model="formData.remark" type="textarea" :rows="2" />
         </el-form-item>
@@ -108,14 +194,14 @@
 <script setup>
 import { ref, reactive, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { getEquipmentPage, getEquipmentById, addEquipment, updateEquipment, deleteEquipment } from '@/api/pipeNetwork/basic'
+import { getEquipmentFullPage, getEquipmentById, addEquipment, updateEquipment, deleteEquipment } from '@/api/pipeNetwork/basic'
 
 const loading = ref(false)
 const tableData = ref([])
 const pageNum = ref(1)
 const pageSize = ref(10)
 const total = ref(0)
-const searchForm = reactive({ equipmentCode: '', equipmentName: '', equipmentModel: '' })
+const searchForm = reactive({ equipmentCode: '', equipmentName: '', equipmentModel: '', manufacturer: '' })
 const dialogVisible = ref(false)
 const dialogTitle = ref('新增')
 const formRef = ref(null)
@@ -130,13 +216,16 @@ const formData = reactive({
 async function loadData() {
   loading.value = true
   try {
-    const res = await getEquipmentPage(pageNum.value, pageSize.value, searchForm)
-    tableData.value = res.data.records || []
-    total.value = res.data.total || 0
+    const res = await getEquipmentFullPage(pageNum.value, pageSize.value, searchForm)
+    tableData.value = (res.data && res.data.records) ? res.data.records : []
+    total.value = (res.data && res.data.total) ? res.data.total : 0
   } catch (e) { console.error(e) } finally { loading.value = false }
 }
 function handleSearch() { pageNum.value = 1; loadData() }
-function handleReset() { Object.assign(searchForm, { equipmentCode: '', equipmentName: '', equipmentModel: '' }); handleSearch() }
+function handleReset() {
+  Object.assign(searchForm, { equipmentCode: '', equipmentName: '', equipmentModel: '', manufacturer: '' })
+  handleSearch()
+}
 function handleAdd() {
   dialogTitle.value = '新增'
   Object.assign(formData, { equipmentId: '', equipmentCode: '', equipmentName: '', equipmentModel: '', equipmentSpec: '', manufacturer: '', equipmentLocation: '', longitude: null, latitude: null, maintainer: '', maintainerPhone: '', remark: '' })
@@ -156,11 +245,8 @@ function handleDelete(row) {
   }).catch(() => {})
 }
 async function handleDetail(row) {
-  try {
-    const res = await getEquipmentById(row.equipmentId)
-    detailData.value = res.data
-    detailVisible.value = true
-  } catch (e) { console.error(e) }
+  detailData.value = row
+  detailVisible.value = true
 }
 
 onMounted(() => { loadData() })
@@ -169,4 +255,5 @@ onMounted(() => { loadData() })
 <style scoped>
 .equipment-container { padding: 16px; }
 .search-card { margin-bottom: 16px; }
+.search-form { display: flex; flex-wrap: wrap; }
 </style>

+ 374 - 0
src/views/subSystem/basic/GasPipePoint.vue

@@ -0,0 +1,374 @@
+<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>
+
+    <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>
+
+    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="650px" destroy-on-close>
+      <el-form :model="formData" :rules="rules" ref="formRef" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="管点编码" prop="pointCode">
+              <el-input v-model="formData.pointCode" placeholder="请输入管点编码" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="管点名称" prop="pointName">
+              <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-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-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-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-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-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-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-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-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-option v-for="d in pointStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
+              </el-select>
+            </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>
+      </template>
+    </el-dialog>
+
+    <el-dialog title="关联设备" v-model="deviceDialogVisible" width="800px" 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>
+        </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-column prop="equipmentType" label="设备类型" width="120" />
+          <el-table-column prop="status" label="状态" width="90">
+            <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">
+            <template #default="{ row: dev }">
+              <el-button type="danger" size="small" @click="handleDeleteDevice(dev)">移除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+      <template #footer>
+        <el-button @click="deviceDialogVisible = false">关闭</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+  getGasPipePointPage, getGasPipePointById, addGasPipePoint,
+  updateGasPipePoint, deleteGasPipePoint, getGasPipePointByNetwork,
+  getPipeNetworkOptions, getPipePointDevices, deletePipePointDeviceRel
+} from '@/api/pipeNetwork/basic'
+import { getDicts } from '@/api/system/dict/data'
+
+const loading = ref(false)
+const tableData = ref([])
+const pageNum = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const networkOptions = ref([])
+
+const searchForm = reactive({
+  pointName: '', pointCode: '', networkId: '', pointType: '', status: ''
+})
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增')
+const formRef = ref(null)
+const formData = reactive({
+  pointId: '', pointCode: '', pointName: '', networkId: '', networkName: '',
+  pointType: '', longitude: null, latitude: null, address: '', diameter: null,
+  material: '', depth: null, buildDate: '', status: '0', remark: ''
+})
+
+const rules = {
+  pointCode: [{ required: true, message: '请输入管点编码', trigger: 'blur' }],
+  pointName: [{ required: true, message: '请输入管点名称', trigger: 'blur' }],
+  networkId: [{ required: true, message: '请选择所属管线', trigger: 'change' }],
+  pointType: [{ required: true, message: '请选择管点类型', trigger: 'change' }]
+}
+
+const deviceDialogVisible = ref(false)
+const currentPoint = ref(null)
+const deviceList = ref([])
+const deviceLoading = ref(false)
+
+const pointTypeDict = ref([])
+const pointStatusDict = ref([])
+
+function getDictLabel(dict, value) {
+  const item = dict.find(d => d.dictValue === String(value))
+  return item ? item.dictLabel : value
+}
+
+async function loadDicts() {
+  const [typeRes, statusRes] = await Promise.all([
+    getDicts('gas_pipe_point_type'),
+    getDicts('gas_pipe_point_status')
+  ])
+  pointTypeDict.value = typeRes.data || []
+  pointStatusDict.value = statusRes.data || []
+}
+
+async function loadNetworkOptions() {
+  try {
+    const res = await getPipeNetworkOptions()
+    networkOptions.value = res.data || []
+  } catch (e) { console.error(e) }
+}
+
+async function loadData() {
+  loading.value = true
+  try {
+    const params = { ...searchForm }
+    const res = await getGasPipePointPage(pageNum.value, pageSize.value, params)
+    tableData.value = (res.data && res.data.records) ? res.data.records : []
+    total.value = (res.data && res.data.total) ? res.data.total : 0
+  } catch (e) {
+    console.error(e)
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleSearch() { pageNum.value = 1; loadData() }
+function handleReset() {
+  Object.assign(searchForm, { pointName: '', pointCode: '', networkId: '', pointType: '', status: '' })
+  handleSearch()
+}
+
+function handleAdd() {
+  dialogTitle.value = '新增管点'
+  Object.assign(formData, {
+    pointId: '', pointCode: '', pointName: '', networkId: '', networkName: '',
+    pointType: '', longitude: null, latitude: null, address: '', diameter: null,
+    material: '', depth: null, buildDate: '', status: '0', remark: ''
+  })
+  dialogVisible.value = true
+}
+
+function handleEdit(row) {
+  dialogTitle.value = '编辑管点'
+  Object.assign(formData, {
+    pointId: row.pointId || '', pointCode: row.pointCode || '', pointName: row.pointName || '',
+    networkId: row.networkId || '', networkName: row.networkName || '',
+    pointType: row.pointType || '', longitude: row.longitude, latitude: row.latitude,
+    address: row.address || '', diameter: row.diameter, material: row.material || '',
+    depth: row.depth, buildDate: row.buildDate || '', status: row.status || '0', remark: row.remark || ''
+  })
+  dialogVisible.value = true
+}
+
+async function handleSubmit() {
+  const valid = await formRef.value.validate().catch(() => false)
+  if (!valid) return
+  try {
+    const selectedNetwork = networkOptions.value.find(o => o.networkId === formData.networkId)
+    if (selectedNetwork) {
+      formData.networkName = selectedNetwork.networkName
+    }
+    if (formData.pointId) {
+      await updateGasPipePoint({ ...formData })
+      ElMessage.success('修改成功')
+    } else {
+      await addGasPipePoint({ ...formData })
+      ElMessage.success('新增成功')
+    }
+    dialogVisible.value = false
+    loadData()
+  } catch (e) {
+    console.error(e)
+  }
+}
+
+function handleDelete(row) {
+  ElMessageBox.confirm(`确认删除管点「${row.pointName}」?`, '提示', { type: 'warning' }).then(async () => {
+    try {
+      await deleteGasPipePoint(row.pointId)
+      ElMessage.success('删除成功')
+      loadData()
+    } catch (e) { console.error(e) }
+  }).catch(() => {})
+}
+
+async function handleViewDevices(row) {
+  currentPoint.value = row
+  deviceDialogVisible.value = true
+  deviceLoading.value = true
+  try {
+    const res = await getPipePointDevices(row.pointId)
+    deviceList.value = res.data || []
+  } catch (e) {
+    deviceList.value = []
+    console.error(e)
+  } finally {
+    deviceLoading.value = false
+  }
+}
+
+function handleAddDevice() {
+  ElMessage.info('请在设备管理页面中关联管点')
+}
+
+async function handleDeleteDevice(dev) {
+  ElMessageBox.confirm('确认移除该设备关联?', '提示', { type: 'warning' }).then(async () => {
+    try {
+      await deletePipePointDeviceRel(dev.relId)
+      ElMessage.success('移除成功')
+      handleViewDevices(currentPoint.value)
+    } catch (e) { console.error(e) }
+  }).catch(() => {})
+}
+
+onMounted(async () => {
+  await Promise.all([loadDicts(), loadNetworkOptions()])
+  loadData()
+})
+</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; }
+</style>

+ 5 - 5
src/views/subSystem/basic/GisDashboard.vue

@@ -368,7 +368,7 @@ const pipePointData = [
   },
   {
     id: 'point-regulator-01', type: 'regulator', pipelineId: 'pipe-high-01',
-    name: '辰州路调压站', code: 'GD-TY-001', road: '辰州中路',
+    name: '辰州路', code: 'GD-TY-001', road: '辰州中路',
     status: '在线', deviceModel: 'RTZ-80/0.4F', caliber: 'DN300→DN200',
     lng: 110.3934, lat: 28.4585
   },
@@ -380,25 +380,25 @@ const pipePointData = [
   },
   {
     id: 'point-regulator-03', type: 'regulator', pipelineId: 'pipe-medium-02',
-    name: '建设路调压', code: 'GD-TY-003', road: '建设西路',
+    name: '建设路调压', code: 'GD-TY-003', road: '建设西路',
     status: '在线', deviceModel: 'RTZ-31/0.4Q', caliber: 'DN160→DN110',
     lng: 110.3840, lat: 28.4588
   },
   {
     id: 'point-flowmeter-01', type: 'flowmeter', pipelineId: 'pipe-high-01',
-    name: '辰州北路计量站', code: 'GD-LL-001', road: '辰州北路',
+    name: '辰州北路站', code: 'GD-LL-001', road: '辰州北路',
     status: '在线', deviceModel: 'LWQZ-150', caliber: 'DN300',
     lng: 110.3927, lat: 28.4721
   },
   {
     id: 'point-flowmeter-02', type: 'flowmeter', pipelineId: 'pipe-medium-01',
-    name: '迎宾西路计量站', code: 'GD-LL-002', road: '迎宾西路',
+    name: '迎宾西路站', code: 'GD-LL-002', road: '迎宾西路',
     status: '在线', deviceModel: 'LWQZ-100', caliber: 'DN200',
     lng: 110.3824, lat: 28.4645
   },
   {
     id: 'point-flowmeter-03', type: 'flowmeter', pipelineId: 'pipe-medium-02',
-    name: '建设东路计量柜', code: 'GD-LL-003', road: '建设东路',
+    name: '建设东路', code: 'GD-LL-003', road: '建设东路',
     status: '在线', deviceModel: 'LWQZ-80', caliber: 'DN110',
     lng: 110.4038, lat: 28.4578
   },

+ 480 - 78
src/views/subSystem/basic/RepairRecord.vue

@@ -1,35 +1,75 @@
 <template>
   <div class="repair-container">
-    <el-card class="search-card">
-      <el-form :inline="true" :model="searchForm">
-        <el-form-item label="管网ID">
-          <el-input v-model="searchForm.networkId" placeholder="请输入管网ID" clearable />
-        </el-form-item>
-        <el-form-item label="维修类型">
-          <el-select v-model="searchForm.repairType" placeholder="请选择" clearable style="width:160px">
-            <el-option v-for="d in repairTypeDict" :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="top-section">
+      <el-card class="topo-card">
+        <template #header>
+          <div class="topo-header">
+            <span class="card-title">燃气管网维修记录拓扑图</span>
+            <div class="topo-legend">
+              <span class="legend-item"><i class="legend-line high"></i> 高压</span>
+              <span class="legend-item"><i class="legend-line medium"></i> 中压</span>
+              <span class="legend-item"><i class="legend-line low"></i> 低压</span>
+              <span class="legend-item"><i class="legend-dot repaired"></i> 有维修</span>
+              <span class="legend-item"><i class="legend-dot point"></i> 管点</span>
+            </div>
+          </div>
+        </template>
+        <div class="map-wrapper">
+          <div id="repairBaiduMap" class="baidu-map"></div>
+          <div v-if="mapError" class="map-error">{{ mapError }}</div>
+        </div>
+      </el-card>
 
+      <div class="charts-column">
+        <el-card class="chart-card">
+          <template #header><span class="card-title">维修统计</span></template>
+          <div id="riskPieChart" style="height:260px"></div>
+        </el-card>
+        <el-card class="chart-card">
+          <template #header><span class="card-title">维修趋势</span></template>
+          <div id="repairTrendChart" style="height:260px"></div>
+        </el-card>
+      </div>
+    </div>
+
+    <!-- 下半部分:搜索 + 表格 -->
     <el-card class="table-card">
-      <el-table :data="tableData" border stripe v-loading="loading">
-        <el-table-column prop="networkId" label="管网ID" min-width="130" />
-        <el-table-column prop="repairDate" label="维修日期" width="110" />
+      <div class="table-toolbar">
+        <div class="table-toolbar-left">
+          <el-form :inline="true" :model="searchForm" class="search-form">
+            <el-form-item label="管网ID">
+              <el-select v-model="searchForm.networkId" placeholder="请选择" clearable style="width:190px" @change="onFilterChange">
+                <el-option v-for="p in pipelineData" :key="p.id" :label="p.id + ' - ' + p.name" :value="p.id" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="维修类型">
+              <el-select v-model="searchForm.repairType" placeholder="请选择" clearable style="width:160px">
+                <el-option v-for="d in repairTypeDict" :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-form-item>
+          </el-form>
+        </div>
+        <div class="table-toolbar-right">
+          <el-button type="success" @click="handleAdd">新增维修记录</el-button>
+        </div>
+      </div>
+
+      <el-table :data="paginatedData" border stripe v-loading="loading" max-height="380" size="small">
+        <el-table-column prop="networkId" label="管网ID" width="100" />
+        <el-table-column prop="repairDate" label="维修日期" width="120" />
         <el-table-column prop="repairType" label="维修类型" width="120">
           <template #default="{ row }">{{ getDictLabel(repairTypeDict, row.repairType) }}</template>
         </el-table-column>
-        <el-table-column prop="repairContent" label="维修内容" min-width="200" show-overflow-tooltip />
-        <el-table-column prop="repairCompany" label="维修单位" min-width="150" />
-        <el-table-column prop="repairCost" label="费用(元)" width="110" />
+        <el-table-column prop="repairContent" label="维修内容" min-width="230" show-overflow-tooltip />
+        <el-table-column prop="repairCompany" label="维修单位" min-width="160" show-overflow-tooltip />
+        <el-table-column prop="repairCost" label="费用(元)" width="110" align="right" />
         <el-table-column prop="repairPerson" label="负责人" width="100" />
-        <el-table-column prop="createTime" label="创建时间" width="160" />
+        <el-table-column prop="createTime" label="创建时间" width="170" />
         <el-table-column label="操作" width="150" fixed="right">
           <template #default="{ row }">
             <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
@@ -40,38 +80,31 @@
       <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"
+        style="margin-top:16px;justify-content:flex-end"
       />
     </el-card>
 
+    <!-- 详情对话框 -->
     <el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px" destroy-on-close>
       <el-form :model="formData" ref="formRef" label-width="100px">
         <el-form-item label="管网ID" prop="networkId">
-          <el-input v-model="formData.networkId" placeholder="请输入管网设施ID" />
+          <el-select v-model="formData.networkId" style="width:100%">
+            <el-option v-for="p in pipelineData" :key="p.id" :label="p.id + ' - ' + p.name" :value="p.id" />
+          </el-select>
         </el-form-item>
         <el-form-item label="维修日期" prop="repairDate">
-          <el-date-picker v-model="formData.repairDate" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" />
+          <el-date-picker v-model="formData.repairDate" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width:100%" />
         </el-form-item>
         <el-form-item label="维修类型" prop="repairType">
-          <el-select v-model="formData.repairType" style="width: 100%">
+          <el-select v-model="formData.repairType" style="width:100%">
             <el-option v-for="d in repairTypeDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
           </el-select>
         </el-form-item>
-        <el-form-item label="维修内容">
-          <el-input v-model="formData.repairContent" type="textarea" :rows="3" />
-        </el-form-item>
-        <el-form-item label="维修单位">
-          <el-input v-model="formData.repairCompany" />
-        </el-form-item>
-        <el-form-item label="费用(元)">
-          <el-input-number v-model="formData.repairCost" :precision="2" :step="100" style="width: 100%" />
-        </el-form-item>
-        <el-form-item label="负责人">
-          <el-input v-model="formData.repairPerson" />
-        </el-form-item>
-        <el-form-item label="备注">
-          <el-input v-model="formData.remark" type="textarea" :rows="2" />
-        </el-form-item>
+        <el-form-item label="维修内容"><el-input v-model="formData.repairContent" type="textarea" :rows="3" /></el-form-item>
+        <el-form-item label="维修单位"><el-input v-model="formData.repairCompany" /></el-form-item>
+        <el-form-item label="费用(元)"><el-input-number v-model="formData.repairCost" :precision="2" :step="100" style="width:100%" /></el-form-item>
+        <el-form-item label="负责人"><el-input v-model="formData.repairPerson" /></el-form-item>
+        <el-form-item label="备注"><el-input v-model="formData.remark" type="textarea" :rows="2" /></el-form-item>
       </el-form>
       <template #footer>
         <el-button @click="dialogVisible = false">取消</el-button>
@@ -82,68 +115,437 @@
 </template>
 
 <script setup>
-import { ref, reactive, onMounted } from 'vue'
+import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
+import * as echarts from 'echarts'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { getRepairRecordPage, addRepairRecord, updateRepairRecord, deleteRepairRecord } from '@/api/pipeNetwork/basic'
-import { getDicts } from '@/api/system/dict/data'
 
+// ==================== 静态数据 ====================
 const loading = ref(false)
-const tableData = ref([])
 const pageNum = ref(1)
 const pageSize = ref(10)
-const total = ref(0)
 const searchForm = reactive({ networkId: '', repairType: '' })
 const dialogVisible = ref(false)
 const dialogTitle = ref('新增')
 const formRef = ref(null)
-const formData = reactive({
-  repairId: '', networkId: '', repairDate: '', repairType: '',
-  repairContent: '', repairCompany: '', repairCost: null, repairPerson: '', remark: ''
-})
+const formData = reactive({ repairId: '', networkId: '', repairDate: '', repairType: '', repairContent: '', repairCompany: '', repairCost: null, repairPerson: '', remark: '' })
 
-const repairTypeDict = ref([])
+const repairTypeDict = ref([
+  { dictValue: '1', dictLabel: '抢修' },
+  { dictValue: '2', dictLabel: '计划维修' },
+  { dictValue: '3', dictLabel: '防腐层修复' },
+  { dictValue: '4', dictLabel: '管件更换' },
+  { dictValue: '5', dictLabel: '焊缝修补' },
+  { dictValue: '6', dictLabel: '阀门维修' }
+])
 function getDictLabel(dict, value) {
   const item = dict.find(d => d.dictValue === String(value))
   return item ? item.dictLabel : value
 }
 
-async function loadDicts() {
-  const res = await getDicts('pipe_repair_type')
-  repairTypeDict.value = res.data
+// ==================== 百度地图配置 ====================
+const centerPoint = { lng: 110.3937, lat: 28.4640 }
+const MAP_BOUNDS = { southWest: { lng: 110.365, lat: 28.452 }, northEast: { lng: 110.420, lat: 28.482 } }
+const MAP_MIN_ZOOM = 13
+const MAP_MAX_ZOOM = 19
+const MAP_DEFAULT_ZOOM = 15
+
+const mapError = ref('')
+let mapInstance = null
+let activeInfoWindow = null
+let overlayClicked = false
+let mapOverlays = []
+
+// ==================== 管网数据 (沅陵县真实坐标) ====================
+const pipelineData = [
+  // 高压管线 — 辰州北路/辰州中路 (拆分为5段 net001-net003)
+  { id: 'net001', name: '辰州北路高压段', pressure: 'high',
+    points: [{lng:110.3918,lat:28.4793},{lng:110.3920,lat:28.4765},{lng:110.3924,lat:28.4743},{lng:110.3927,lat:28.4721},{lng:110.3931,lat:28.4698}] },
+  { id: 'net002', name: '辰州中路高压A段', pressure: 'high',
+    points: [{lng:110.3931,lat:28.4698},{lng:110.3934,lat:28.4675},{lng:110.3936,lat:28.4652},{lng:110.3937,lat:28.4630}] },
+  { id: 'net003', name: '辰州中路高压B段', pressure: 'high',
+    points: [{lng:110.3937,lat:28.4630},{lng:110.3935,lat:28.4607},{lng:110.3934,lat:28.4585},{lng:110.3935,lat:28.4576}] },
+  // 次高压/中压 — 古城路 (拆分为4段 net004-net007)
+  { id: 'net004', name: '古城北路中压段', pressure: 'medium',
+    points: [{lng:110.3980,lat:28.4790},{lng:110.3979,lat:28.4765},{lng:110.3977,lat:28.4741},{lng:110.3975,lat:28.4717}] },
+  { id: 'net005', name: '古城中路中压A段', pressure: 'medium',
+    points: [{lng:110.3975,lat:28.4717},{lng:110.3973,lat:28.4693},{lng:110.3970,lat:28.4669}] },
+  { id: 'net006', name: '古城中路中压B段', pressure: 'medium',
+    points: [{lng:110.3970,lat:28.4669},{lng:110.3968,lat:28.4645},{lng:110.3965,lat:28.4622}] },
+  { id: 'net007', name: '古城南路中压段', pressure: 'medium',
+    points: [{lng:110.3965,lat:28.4622},{lng:110.3963,lat:28.4598},{lng:110.3962,lat:28.4575}] },
+  // 中压 — 迎宾路 (拆分为3段 net008-net010)
+  { id: 'net008', name: '迎宾西路中压段', pressure: 'medium',
+    points: [{lng:110.3732,lat:28.4650},{lng:110.3778,lat:28.4648},{lng:110.3824,lat:28.4645},{lng:110.3870,lat:28.4642}] },
+  { id: 'net009', name: '县政府迎宾路中压段', pressure: 'medium',
+    points: [{lng:110.3870,lat:28.4642},{lng:110.3916,lat:28.4640},{lng:110.3962,lat:28.4637}] },
+  { id: 'net010', name: '迎宾东路中压段', pressure: 'medium',
+    points: [{lng:110.3962,lat:28.4637},{lng:110.4008,lat:28.4634},{lng:110.4054,lat:28.4630},{lng:110.4098,lat:28.4627},{lng:110.4120,lat:28.4625}] },
+  // 中压 — 建设路 (拆分为3段 net011-net013)
+  { id: 'net011', name: '建设西路中压段', pressure: 'medium',
+    points: [{lng:110.3748,lat:28.4583},{lng:110.3794,lat:28.4586},{lng:110.3840,lat:28.4588},{lng:110.3886,lat:28.4589}] },
+  { id: 'net012', name: '建设路中段', pressure: 'medium',
+    points: [{lng:110.3886,lat:28.4589},{lng:110.3912,lat:28.4586},{lng:110.3937,lat:28.4582},{lng:110.3966,lat:28.4579}] },
+  { id: 'net013', name: '建设东路中压段', pressure: 'medium',
+    points: [{lng:110.3966,lat:28.4579},{lng:110.4002,lat:28.4581},{lng:110.4038,lat:28.4578},{lng:110.4076,lat:28.4575}] },
+  // 低压 — 滨江路 (拆分为2段 net014-net015)
+  { id: 'net014', name: '滨江路西低压段', pressure: 'low',
+    points: [{lng:110.3762,lat:28.4569},{lng:110.3804,lat:28.4570},{lng:110.3846,lat:28.4571},{lng:110.3882,lat:28.4568},{lng:110.3918,lat:28.4565}] },
+  { id: 'net015', name: '滨江路东低压段', pressure: 'low',
+    points: [{lng:110.3918,lat:28.4565},{lng:110.3954,lat:28.4562},{lng:110.3990,lat:28.4560},{lng:110.4028,lat:28.4559},{lng:110.4066,lat:28.4557},{lng:110.4092,lat:28.4556}] },
+  // 低压 — 天宁路 (拆分为2段 net016-net017)
+  { id: 'net016', name: '天宁西路低压段', pressure: 'low',
+    points: [{lng:110.3828,lat:28.4704},{lng:110.3870,lat:28.4702},{lng:110.3912,lat:28.4700},{lng:110.3954,lat:28.4697}] },
+  { id: 'net017', name: '天宁东路低压段', pressure: 'low',
+    points: [{lng:110.3954,lat:28.4697},{lng:110.3996,lat:28.4695},{lng:110.4038,lat:28.4693},{lng:110.4078,lat:28.4691},{lng:110.4092,lat:28.4690}] },
+]
+
+// 管点数据 (管网节点位置)
+const pipePointData = [
+  { name: '辰州北路阀门', lng: 110.3924, lat: 28.4743, pipelineId: 'net001' },
+  { name: '辰州中路阀门', lng: 110.3936, lat: 28.4652, pipelineId: 'net002' },
+  { name: '辰州路调压站', lng: 110.3934, lat: 28.4585, pipelineId: 'net003' },
+  { name: '古城北路阀门', lng: 110.3977, lat: 28.4741, pipelineId: 'net004' },
+  { name: '古城路调压站', lng: 110.3963, lat: 28.4598, pipelineId: 'net007' },
+  { name: '迎宾西路站', lng: 110.3824, lat: 28.4645, pipelineId: 'net008' },
+  { name: '迎宾东路阀门', lng: 110.4054, lat: 28.4630, pipelineId: 'net010' },
+  { name: '建设路点', lng: 110.3840, lat: 28.4588, pipelineId: 'net011' },
+  { name: '县政府终端', lng: 110.3937, lat: 28.4582, pipelineId: 'net012' },
+  { name: '建设东路点', lng: 110.4038, lat: 28.4578, pipelineId: 'net013' },
+  { name: '天宁东路终端', lng: 110.4092, lat: 28.4690, pipelineId: 'net017' },
+]
+
+// ==================== 维修记录 ====================
+const tableData = ref([
+  { repairId: '1', networkId: 'net001', repairDate: '2026-06-10', repairType: '1', repairContent: '辰州北路高压段焊缝渗漏,管道焊接接头处出现裂纹,停气抢修并更换受损管段1.2m', repairCompany: '沅陵燃气工程有限公司', repairCost: 12500, repairPerson: '张建国', createTime: '2026-06-10 14:30:22', remark: '夜间施工,影响200户居民用气3小时' },
+  { repairId: '2', networkId: 'net005', repairDate: '2026-06-05', repairType: '3', repairContent: '古城中路中压A段防腐层老化脱落,管体局部锈蚀深度1.8mm,防腐层整体修复', repairCompany: '湖南城市燃气技术公司', repairCost: 38600, repairPerson: '李志强', createTime: '2026-06-05 09:15:40', remark: '修复长度约85m' },
+  { repairId: '3', networkId: 'net008', repairDate: '2026-05-28', repairType: '4', repairContent: '迎宾西路中压段三通管件更换,原管件因地基沉降导致应力开裂,更换为加强型管件', repairCompany: '怀化兴燃工程有限公司', repairCost: 8200, repairPerson: '王立新', createTime: '2026-05-28 16:45:10', remark: '' },
+  { repairId: '4', networkId: 'net012', repairDate: '2026-05-20', repairType: '1', repairContent: '建设路中段第三方施工破坏,DN110 PE管被挖掘机铲断,紧急停气抢修', repairCompany: '沅陵燃气工程有限公司', repairCost: 15000, repairPerson: '赵明辉', createTime: '2026-05-20 03:22:55', remark: '第三方施工未报备' },
+  { repairId: '5', networkId: 'net003', repairDate: '2026-05-15', repairType: '2', repairContent: '辰州中路高压B段计划停气检修,更换老化阀门2台,管道内检测并清管', repairCompany: '湖南省特种设备检测院', repairCost: 52000, repairPerson: '刘伟东', createTime: '2026-05-15 08:00:00', remark: '年度计划检修,提前72小时通知用户' },
+  { repairId: '6', networkId: 'net009', repairDate: '2026-05-08', repairType: '5', repairContent: '县政府迎宾路中压段环焊缝异常,超声检测发现焊缝内部缺陷,焊缝重新焊接', repairCompany: '湖南城市燃气技术公司', repairCost: 18600, repairPerson: '李志强', createTime: '2026-05-08 11:20:18', remark: '焊缝缺陷深度2.3mm' },
+  { repairId: '7', networkId: 'net015', repairDate: '2026-04-30', repairType: '6', repairContent: '滨江路东低压段阀门操作卡涩,阀杆填料泄漏,拆解阀门更换填料及O型密封圈', repairCompany: '怀化兴燃工程有限公司', repairCost: 4500, repairPerson: '王立新', createTime: '2026-04-30 14:10:33', remark: '' },
+  { repairId: '8', networkId: 'net011', repairDate: '2026-04-22', repairType: '3', repairContent: '建设西路中压段管道外防腐层破损,杂散电流干扰导致电化学腐蚀,修复防腐层并加装牺牲阳极', repairCompany: '湖南城市燃气技术公司', repairCost: 28500, repairPerson: '李志强', createTime: '2026-04-22 10:05:00', remark: '腐蚀深度1.5mm' },
+  { repairId: '9', networkId: 'net002', repairDate: '2026-04-15', repairType: '1', repairContent: '辰州中路高压A段管道泄漏报警,法兰连接处密封垫老化泄漏,紧急更换金属缠绕垫', repairCompany: '沅陵燃气工程有限公司', repairCost: 6300, repairPerson: '张建国', createTime: '2026-04-15 21:08:42', remark: '泄漏量约0.3m³' },
+  { repairId: '10', networkId: 'net006', repairDate: '2026-04-08', repairType: '2', repairContent: '古城中路中压B段年度维护,全线管道壁厚检测,阴极保护系统检测,更换测试桩3个', repairCompany: '湖南省特种设备检测院', repairCost: 22000, repairPerson: '刘伟东', createTime: '2026-04-08 09:00:00', remark: '壁厚检测结果符合GB50028' },
+  { repairId: '11', networkId: 'net014', repairDate: '2026-03-28', repairType: '4', repairContent: '滨江路西低压段PE管老化更换,原管道使用年限已到设计寿命,整体更换DN160 PE100新管', repairCompany: '沅陵燃气工程有限公司', repairCost: 78500, repairPerson: '赵明辉', createTime: '2026-03-28 07:30:00', remark: '更换长度约210m' },
+  { repairId: '12', networkId: 'net004', repairDate: '2026-03-18', repairType: '6', repairContent: '古城北路中压段阀门井内积水导致阀门锈蚀,清理井室并更换锈蚀螺栓及附件', repairCompany: '怀化兴燃工程有限公司', repairCost: 3500, repairPerson: '王立新', createTime: '2026-03-18 13:25:10', remark: '' },
+  { repairId: '13', networkId: 'net010', repairDate: '2026-03-10', repairType: '5', repairContent: '迎宾东路中压段直缝焊管焊缝补强,焊缝余高不符合规范,打磨处理后进行补焊', repairCompany: '湖南城市燃气技术公司', repairCost: 9800, repairPerson: '李志强', createTime: '2026-03-10 15:40:05', remark: '焊缝长度28mm' },
+  { repairId: '14', networkId: 'net016', repairDate: '2026-02-25', repairType: '3', repairContent: '天宁西路低压段沿河侧管道外防腐保温层修复,河水浸泡导致保温层脱落约40m', repairCompany: '沅陵燃气工程有限公司', repairCost: 33500, repairPerson: '张建国', createTime: '2026-02-25 10:20:30', remark: '枯水期施工' },
+  { repairId: '15', networkId: 'net007', repairDate: '2026-02-18', repairType: '2', repairContent: '古城南路中压段穿越路口管段套管更换,原钢套管腐蚀穿孔,换用加强级3PE防腐套管', repairCompany: '湖南省特种设备检测院', repairCost: 16500, repairPerson: '刘伟东', createTime: '2026-02-18 08:50:15', remark: '' },
+  { repairId: '16', networkId: 'net017', repairDate: '2026-02-08', repairType: '4', repairContent: '天宁东路低压段凝水缸更换,冬季冻裂导致凝水缸壳体破损,更换为抗冻型凝水缸', repairCompany: '怀化兴燃工程有限公司', repairCost: 7200, repairPerson: '王立新', createTime: '2026-02-08 11:15:00', remark: '' },
+  { repairId: '17', networkId: 'net013', repairDate: '2026-01-22', repairType: '1', repairContent: '建设东路中压段地基下沉导致管道变形,开挖后重新敷设管基并加装波纹补偿器', repairCompany: '沅陵燃气工程有限公司', repairCost: 25600, repairPerson: '赵明辉', createTime: '2026-01-22 16:35:48', remark: '沉降量约80mm' },
+  { repairId: '18', networkId: 'net001', repairDate: '2026-01-10', repairType: '2', repairContent: '辰州北路高压段阴极保护系统维护,更换阳极地床,测试保护电位', repairCompany: '湖南省特种设备检测院', repairCost: 11200, repairPerson: '刘伟东', createTime: '2026-01-10 09:30:00', remark: '保护电位-1.15V~-1.22V' },
+])
+
+const pressureColors = { high: '#e63946', medium: '#f77f00', low: '#2a9d8f' }
+const repairedNetworkIds = computed(() => { const s = new Set(); tableData.value.forEach(r => s.add(r.networkId)); return s })
+
+function getPipelineName(id) { const p = pipelineData.find(p => p.id === id); return p ? p.name : id }
+
+// ==================== 过滤分页 ====================
+const filteredData = computed(() => {
+  let data = tableData.value
+  if (searchForm.networkId) data = data.filter(r => r.networkId === searchForm.networkId)
+  if (searchForm.repairType) data = data.filter(r => r.repairType === searchForm.repairType)
+  return data.sort((a, b) => b.createTime.localeCompare(a.createTime))
+})
+const paginatedData = computed(() => filteredData.value.slice((pageNum.value - 1) * pageSize.value, pageNum.value * pageSize.value))
+const total = computed(() => filteredData.value.length)
+
+// ==================== 百度地图 ====================
+function waitForBMapGL() {
+  if (typeof BMapGL !== 'undefined') { initMap(); return }
+  let attempts = 0
+  const timer = setInterval(() => {
+    attempts++
+    if (typeof BMapGL !== 'undefined') { clearInterval(timer); initMap(); return }
+    if (attempts >= 50) { clearInterval(timer); mapError.value = '百度地图脚本加载超时,请刷新页面重试。' }
+  }, 200)
+}
+
+function closeInfoWindow() {
+  if (activeInfoWindow) { try { activeInfoWindow.close(); mapInstance.removeOverlay(activeInfoWindow) } catch (_) {}; activeInfoWindow = null }
 }
 
-async function loadData() {
-  loading.value = true
+function getMidPoint(points) {
+  let lng = 0, lat = 0
+  points.forEach(p => { lng += p.lng; lat += p.lat })
+  return { lng: lng / points.length, lat: lat / points.length }
+}
+
+function initMap() {
+  const el = document.getElementById('repairBaiduMap')
+  if (!el) { mapError.value = '地图容器未找到。'; return }
+
+  try { mapInstance = new BMapGL.Map('repairBaiduMap', { enableMapClick: true }) } catch (e) {
+    console.error('创建地图实例失败:', e); mapError.value = '百度地图初始化失败,请检查AK和网络连接。'; return
+  }
+
+  try { mapInstance.centerAndZoom(new BMapGL.Point(centerPoint.lng, centerPoint.lat), MAP_DEFAULT_ZOOM); mapInstance.enableScrollWheelZoom(true) } catch (e) { console.error('设置中心失败:', e) }
+
+  try { if (typeof mapInstance.setMinZoom === 'function') mapInstance.setMinZoom(MAP_MIN_ZOOM); if (typeof mapInstance.setMaxZoom === 'function') mapInstance.setMaxZoom(MAP_MAX_ZOOM) } catch (e) {}
+
   try {
-    const res = await getRepairRecordPage(pageNum.value, pageSize.value, searchForm)
-    tableData.value = res.data.records || []
-    total.value = res.data.total || 0
-  } catch (e) { console.error(e) } finally { loading.value = false }
+    if (typeof mapInstance.setMaxBounds === 'function') {
+      const sw = new BMapGL.Point(MAP_BOUNDS.southWest.lng, MAP_BOUNDS.southWest.lat)
+      const ne = new BMapGL.Point(MAP_BOUNDS.northEast.lng, MAP_BOUNDS.northEast.lat)
+      mapInstance.setMaxBounds(new BMapGL.Bounds(sw, ne))
+    }
+  } catch (e) {}
+
+  mapInstance.addEventListener('click', () => {
+    if (overlayClicked) { overlayClicked = false; return }
+    closeInfoWindow()
+  })
+
+  try { renderMapScene() } catch (e) { console.error('渲染地图失败:', e); mapError.value = '地图要素渲染失败。' }
+}
+
+function renderMapScene() {
+  if (!mapInstance || typeof BMapGL === 'undefined') return
+  closeInfoWindow()
+  try { mapInstance.clearOverlays() } catch (_) {}
+  mapOverlays = []
+
+  // 绘制管线
+  pipelineData.forEach(pipe => {
+    try {
+      const hasRepair = repairedNetworkIds.value.has(pipe.id)
+      const pts = pipe.points.map(p => new BMapGL.Point(p.lng, p.lat))
+      const polyline = new BMapGL.Polyline(pts, {
+        strokeColor: hasRepair ? '#f59e0b' : pressureColors[pipe.pressure],
+        strokeWeight: hasRepair ? 5 : 3.5,
+        strokeOpacity: 0.9
+      })
+      mapInstance.addOverlay(polyline)
+      mapOverlays.push({ overlay: polyline, pipe })
+
+      polyline.addEventListener('click', (e) => {
+        overlayClicked = true
+        if (e && e.domEvent) e.domEvent.stopPropagation()
+        closeInfoWindow()
+        const mid = getMidPoint(pipe.points)
+        const count = tableData.value.filter(r => r.networkId === pipe.id).length
+        const repairList = tableData.value.filter(r => r.networkId === pipe.id).slice(0, 3).map(r => `${r.repairDate} ${getDictLabel(repairTypeDict, r.repairType)}`).join('<br>')
+        const html = `<div style="font-size:13px;max-width:300px;"><b>${pipe.id}</b> ${pipe.name}<br>
+          <span style="color:${pressureColors[pipe.pressure]};font-weight:bold;">${pipe.pressure === 'high' ? '高压' : pipe.pressure === 'medium' ? '中压' : '低压'}</span>
+          &nbsp;累计维修 <b style="color:#e63946;">${count}</b> 次
+          ${count > 0 ? '<br><span style="color:#909399;font-size:11px;">' + repairList + '</span>' : ''}</div>`
+        activeInfoWindow = new BMapGL.InfoWindow(html, { width: 260 })
+        mapInstance.openInfoWindow(activeInfoWindow, new BMapGL.Point(mid.lng, mid.lat))
+        // 筛选表格
+        searchForm.networkId = pipe.id
+        pageNum.value = 1
+      })
+    } catch (_) {}
+  })
+
+  // 绘制管点
+  pipePointData.forEach(point => {
+    try {
+      const hasRepair = repairedNetworkIds.value.has(point.pipelineId)
+      const pt = new BMapGL.Point(point.lng, point.lat)
+      const color = hasRepair ? '#f59e0b' : '#3b82f6'
+      const size = hasRepair ? 10 : 7
+      const label = new BMapGL.Label(point.name, {
+        position: pt,
+        offset: new BMapGL.Size(-22, -22)
+      })
+      label.setStyle({
+        color: '#fff', fontSize: '10px', padding: '2px 6px',
+        backgroundColor: color, borderRadius: '3px', whiteSpace: 'nowrap',
+        border: hasRepair ? '2px solid #fff' : 'none', boxShadow: hasRepair ? '0 0 6px rgba(245,158,11,0.6)' : 'none'
+      })
+      mapInstance.addOverlay(label)
+      mapOverlays.push({ overlay: label, point })
+
+      label.addEventListener('click', (e) => {
+        overlayClicked = true
+        if (e && e.domEvent) e.domEvent.stopPropagation()
+        closeInfoWindow()
+        const pipeName = getPipelineName(point.pipelineId)
+        const count = tableData.value.filter(r => r.networkId === point.pipelineId).length
+        const html = `<div style="font-size:13px;"><b>${point.name}</b><br>所属管线: ${point.pipelineId} ${pipeName}<br>维修记录: <b style="color:#e63946;">${count}</b> 次</div>`
+        activeInfoWindow = new BMapGL.InfoWindow(html, { width: 240 })
+        mapInstance.openInfoWindow(activeInfoWindow, pt)
+        searchForm.networkId = point.pipelineId
+        pageNum.value = 1
+      })
+    } catch (_) {}
+  })
+
+  // 维修标记点 (有维修记录的管线中点位置)
+  pipelineData.forEach(pipe => {
+    if (!repairedNetworkIds.value.has(pipe.id)) return
+    try {
+      const mid = getMidPoint(pipe.points)
+      const pt = new BMapGL.Point(mid.lng, mid.lat)
+      const count = tableData.value.filter(r => r.networkId === pipe.id).length
+      const marker = new BMapGL.Label(String(count), {
+        position: pt,
+        offset: new BMapGL.Size(-12, -12)
+      })
+      marker.setStyle({
+        color: '#fff', fontSize: '11px', fontWeight: 'bold',
+        backgroundColor: '#e63946', borderRadius: '12px',
+        padding: '2px 7px', border: '2px solid #fff',
+        boxShadow: '0 0 8px rgba(230,57,70,0.5)'
+      })
+      mapInstance.addOverlay(marker)
+      mapOverlays.push({ overlay: marker, pipe })
+    } catch (_) {}
+  })
+}
+
+// ==================== ECharts ====================
+let riskPieChart = null
+let repairTrendChart = null
+
+function initRiskPie() {
+  const dom = document.getElementById('riskPieChart')
+  if (!dom) return
+  if (riskPieChart) riskPieChart.dispose()
+  riskPieChart = echarts.init(dom)
+  riskPieChart.setOption({
+    tooltip: { trigger: 'item', formatter: '{b}: {c} 次 ({d}%)' },
+    legend: { orient: 'vertical', right: 6, top: 'center', itemWidth: 10, itemHeight: 10, textStyle: { color: '#606a78', fontSize: 11 }, itemGap: 14 },
+    series: [{
+      type: 'pie', radius: ['50%', '76%'], center: ['38%', '50%'],
+      avoidLabelOverlap: false, itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 3 },
+      label: { show: false }, emphasis: { scale: true, scaleSize: 6 },
+      data: [
+        { value: 5, name: '抢修', itemStyle: { color: '#e63946' } },
+        { value: 5, name: '计划维修', itemStyle: { color: '#457b9d' } },
+        { value: 4, name: '防腐层修复', itemStyle: { color: '#f77f00' } },
+        { value: 4, name: '管件更换', itemStyle: { color: '#2a9d8f' } },
+        { value: 2, name: '焊缝修补', itemStyle: { color: '#6c757d' } },
+        { value: 2, name: '阀门维修', itemStyle: { color: '#b08968' } }
+      ]
+    }]
+  })
+}
+
+function initRepairTrend() {
+  const dom = document.getElementById('repairTrendChart')
+  if (!dom) return
+  if (repairTrendChart) repairTrendChart.dispose()
+  repairTrendChart = echarts.init(dom)
+  const months = ['1月', '2月', '3月', '4月', '5月', '6月']
+  repairTrendChart.setOption({
+    tooltip: { trigger: 'axis' },
+    legend: { top: 0, textStyle: { color: '#606a78', fontSize: 11 } },
+    grid: { left: 44, right: 20, top: 36, bottom: 32 },
+    xAxis: { type: 'category', data: months, boundaryGap: false, axisLabel: { color: '#9098a6', fontSize: 11 }, axisLine: { lineStyle: { color: '#d0d5dd' } } },
+    yAxis: { type: 'value', name: '次数', minInterval: 1, axisLabel: { color: '#9098a6', fontSize: 11 }, splitLine: { lineStyle: { color: '#f0f2f6', type: 'dashed' } } },
+    series: [
+      { name: '高压', type: 'line', data: [2, 0, 0, 1, 1, 2], smooth: true, symbol: 'circle', symbolSize: 7, lineStyle: { width: 3, color: '#e63946' }, itemStyle: { color: '#e63946', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(230,57,70,0.12)' }, { offset: 1, color: 'rgba(230,57,70,0)' }]) } },
+      { name: '中压', type: 'line', data: [2, 1, 1, 3, 2, 1], smooth: true, symbol: 'circle', symbolSize: 7, lineStyle: { width: 3, color: '#f77f00' }, itemStyle: { color: '#f77f00', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(247,127,0,0.12)' }, { offset: 1, color: 'rgba(247,127,0,0)' }]) } },
+      { name: '低压', type: 'line', data: [1, 2, 1, 2, 1, 2], smooth: true, symbol: 'circle', symbolSize: 7, lineStyle: { width: 3, color: '#2a9d8f' }, itemStyle: { color: '#2a9d8f', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(42,157,143,0.12)' }, { offset: 1, color: 'rgba(42,157,143,0)' }]) } }
+    ]
+  })
+}
+
+function initCharts() { try { initRiskPie() } catch (e) { console.warn('饼图失败:', e) }; try { initRepairTrend() } catch (e) { console.warn('趋势图失败:', e) } }
+function resizeCharts() { riskPieChart?.resize(); repairTrendChart?.resize() }
+
+// ==================== 操作 ====================
+function onFilterChange() { pageNum.value = 1 }
+function handleSearch() { pageNum.value = 1 }
+function handleReset() {
+  searchForm.networkId = ''; searchForm.repairType = ''; pageNum.value = 1
+  nextTick(() => renderMapScene())
 }
-function handleSearch() { pageNum.value = 1; loadData() }
-function handleReset() { Object.assign(searchForm, { networkId: '', repairType: '' }); handleSearch() }
 function handleAdd() {
-  dialogTitle.value = '新增'
+  dialogTitle.value = '新增'; dialogVisible.value = true
   Object.assign(formData, { repairId: '', networkId: '', repairDate: '', repairType: '', repairContent: '', repairCompany: '', repairCost: null, repairPerson: '', remark: '' })
-  dialogVisible.value = true
 }
-function handleEdit(row) { dialogTitle.value = '编辑'; Object.assign(formData, row); dialogVisible.value = true }
-async function handleSubmit() {
-  try {
-    if (formData.repairId) { await updateRepairRecord(formData); ElMessage.success('修改成功') }
-    else { await addRepairRecord(formData); ElMessage.success('新增成功') }
-    dialogVisible.value = false; loadData()
-  } catch (e) { console.error(e) }
+function handleEdit(row) { dialogTitle.value = '编辑'; dialogVisible.value = true; Object.assign(formData, { ...row }) }
+function handleSubmit() {
+  if (formData.repairId) {
+    const idx = tableData.value.findIndex(r => r.repairId === formData.repairId)
+    if (idx >= 0) tableData.value[idx] = { ...formData }
+    ElMessage.success('修改成功')
+  } else {
+    formData.repairId = String(Date.now())
+    formData.createTime = new Date().toLocaleString('zh-CN').replace(/\//g, '-')
+    tableData.value.unshift({ ...formData })
+    ElMessage.success('新增成功')
+  }
+  dialogVisible.value = false
+  nextTick(() => { renderMapScene(); initCharts() })
 }
 function handleDelete(row) {
-  ElMessageBox.confirm('确认删除该维修记录?', '提示', { type: 'warning' }).then(async () => {
-    await deleteRepairRecord(row.repairId); ElMessage.success('删除成功'); loadData()
+  ElMessageBox.confirm('确认删除该维修记录?', '提示', { type: 'warning' }).then(() => {
+    tableData.value = tableData.value.filter(r => r.repairId !== row.repairId)
+    ElMessage.success('删除成功')
+    nextTick(() => { renderMapScene(); initCharts() })
   }).catch(() => {})
 }
-onMounted(async () => { await loadDicts(); loadData() })
+
+// ==================== 生命周期 ====================
+let resizeTimer = null
+onMounted(async () => {
+  await nextTick()
+  waitForBMapGL()
+  initCharts()
+  window.addEventListener('resize', () => {
+    clearTimeout(resizeTimer)
+    resizeTimer = setTimeout(() => {
+      resizeCharts()
+      if (mapInstance) {
+        try { mapInstance.resize() } catch (_) {}
+        try { mapInstance.setViewport([new BMapGL.Point(centerPoint.lng, centerPoint.lat)], { zoom: mapInstance.getZoom() }) } catch (_) {}
+      }
+    }, 200)
+  })
+})
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', resizeCharts)
+  riskPieChart?.dispose(); repairTrendChart?.dispose()
+  if (mapInstance) { try { mapInstance.clearOverlays() } catch (_) {}; mapInstance = null }
+})
 </script>
 
 <style scoped>
-.repair-container { padding: 16px; }
-.search-card { margin-bottom: 16px; }
+.repair-container { padding: 16px; display: flex; flex-direction: column; gap: 16px; background: #f0f2f6; min-height: calc(100vh - 84px); }
+
+/* ========== 上半部分 ========== */
+.top-section { display: flex; gap: 16px; align-items: stretch; }
+.topo-card { flex: 1; min-width: 0; display: flex; flex-direction: column; }
+.charts-column { width: 340px; flex-shrink: 0; display: flex; flex-direction: column; gap: 16px; }
+.chart-card { flex: 1; }
+.card-title { font-size: 15px; font-weight: 700; color: #1f2d3d; }
+
+.topo-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; }
+.topo-legend { display: flex; gap: 14px; flex-wrap: wrap; }
+.legend-item { font-size: 11px; color: #606a78; display: flex; align-items: center; gap: 5px; }
+.legend-line { display: inline-block; width: 24px; height: 3px; border-radius: 2px; }
+.legend-line.high { background: #e63946; }
+.legend-line.medium { background: #f77f00; }
+.legend-line.low { background: #2a9d8f; }
+.legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
+.legend-dot.repaired { background: #f59e0b; border: 2px solid #fff; box-shadow: 0 0 4px rgba(245,158,11,0.5); }
+.legend-dot.point { background: #3b82f6; border: 2px solid #fff; }
+
+.map-wrapper { position: relative; width: 100%; height: 100%; min-height: 460px; border-radius: 6px; overflow: hidden; }
+.baidu-map { width: 100%; height: 100%; min-height: 460px; }
+.map-error { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: #fafbfc; color: #e63946; font-size: 14px; font-weight: 600; }
+.topo-card { display: flex; flex-direction: column; }
+:deep(.topo-card .el-card__body) { flex: 1; display: flex; padding: 0; overflow: hidden; }
+
+/* ========== 表格 ========== */
+.table-toolbar { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; }
+.search-form { margin-bottom: 0; }
+:deep(.search-form .el-form-item) { margin-bottom: 0; }
+
+:deep(.el-card__header) { padding: 12px 18px; border-bottom: 1px solid #f0f2f6; flex-shrink: 0; }
+:deep(.topo-card .el-card__body) { flex: 1; display: flex; padding: 0; overflow: hidden; }
+:deep(.chart-card .el-card__body) { padding: 12px; }
+:deep(.table-card .el-card__body) { padding: 12px; }
+
+@media (max-width: 1400px) {
+  .top-section { flex-direction: column; }
+  .map-wrapper { height: 400px; }
+  .charts-column { width: 100%; flex-direction: row; }
+  .charts-column .chart-card { flex: 1; min-width: 300px; }
+}
 </style>