null před 2 týdny
rodič
revize
8d470d4768

+ 23 - 0
src/api/drainage.js

@@ -284,3 +284,26 @@ export function bindPumpStation(data) {
 export function addWorkOrder(data) {
   return request({ url: '/manhole/workOrder/addWorkOrder', method: 'post', data })
 }
+
+// ============ 预警阈值配置 ============
+export function getThresholdPage(pageNum, pageSize, params) {
+  return request({ url: '/warningThreshold/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
+}
+export function getThresholdModulePage(pageNum, pageSize, typeName, params) {
+  return request({ url: '/warningThreshold/findByModulePage', method: 'get', params: { pageNum, pageSize, typeName, ...params } })
+}
+export function getThresholdList(params) {
+  return request({ url: '/warningThreshold/getWarningThresholdList', method: 'get', params })
+}
+export function getThresholdById(id) {
+  return request({ url: '/warningThreshold/getById/' + id, method: 'get' })
+}
+export function saveThreshold(data) {
+  return request({ url: '/warningThreshold/save', method: 'post', data })
+}
+export function updateThreshold(data) {
+  return request({ url: '/warningThreshold/update', method: 'post', data })
+}
+export function deleteThreshold(ids) {
+  return request({ url: '/warningThreshold/deleteBatch', method: 'post', data: ids })
+}

+ 139 - 7
src/api/pipeNetwork/basic.js

@@ -46,24 +46,111 @@ export function deleteManhole(id) {
   return request({ url: '/api/manhole/deleteById', method: 'delete', params: { id } })
 }
 
-// ============ 窨井-设备关联 ============
+// ============ 窨井井下设备关联 ============
+// 查询设备已关联的井下设备列表
+export function getManholeDeviceRels(equipmentId) {
+  return request({ url: '/manhole/device/rel/list/' + equipmentId, method: 'get' })
+}
+// 搜索可关联设备(排除窨井类型5/9/10,排除已关联设备ID)
+export function searchManholeRelDevices(params) {
+  return request({ url: '/manhole/device/rel/search', method: 'get', params })
+}
+// 查询窨井关联的设备列表(兼容 Manhole.vue,manholeId 为 ManholeBase 的 ID)
 export function getManholeDevices(manholeId) {
-  return request({ url: '/api/manhole/device/list/' + manholeId, method: 'get' })
+  return request({ url: '/manhole/device/rel/list/' + manholeId, method: 'get' })
 }
+// 新增窨井井下设备关联
 export function addManholeDeviceRel(data) {
-  return request({ url: '/api/manhole/device/save', method: 'post', data })
-}
-export function addManholeDeviceRelBatch(dataList) {
-  return request({ url: '/api/manhole/device/save-batch', method: 'post', data: dataList })
+  return request({ url: '/manhole/device/rel/save', method: 'post', data })
 }
+// 删除窨井井下设备关联
 export function deleteManholeDeviceRel(relId) {
-  return request({ url: '/api/manhole/device/delete', method: 'delete', params: { relId } })
+  return request({ url: '/manhole/device/rel/delete', method: 'delete', params: { relId } })
 }
 
 // ============ 井盖监测设备阈值 ============
+// 监测设备台账分页(后端已按排水/供水窨井类型过滤,isQueryManholeData=true 返回监测数据)
+export function getManholeDevicePage(data) {
+  return request({ url: '/manhole/device/findByPage', method: 'post', data })
+}
+// 监测设备台账详情
+export function getManholeDeviceById(id) {
+  return request({ url: '/manhole/device/getById/' + id, method: 'get' })
+}
+// 窨井设备统计:区域分布(数量/占比)+ 类型分布(数量/占比)
+export function getManholeDeviceStatistics(data) {
+  return request({ url: '/manhole/device/statistics', method: 'post', data })
+}
 export function getManholeAlarmDeviceList(data = {}) {
   return request({ url: '/manhole/device/getManholeDataList', method: 'post', data })
 }
+
+// 查询窨井关联设备的井下最新监测数据(e_real_time_data / radar_data)
+export function getManholeRelDeviceData(equipmentId) {
+  return request({ url: '/manhole/device/rel/data/' + equipmentId, method: 'get' })
+}
+// 态势图层 - 获取设备列表和统计(专用于 alarmSanalysis.vue)
+export function getManholeLayerData(data = {}) {
+  return request({ url: '/manhole/layer/getManholeLayerData', method: 'post', data })
+}
+/**
+ * 窨井盖实时监测 - 获取设备报警监控列表
+ *
+ * 业务逻辑:
+ *   查询所有窨井类型(equipment_type_id IN (9,10))设备的 jg_device_data 最新一条记录
+ *   根据以下字段判定告警等级:
+ *     - 倾斜角度: tilt_angle > angle_alarm_threshold → 严重告警
+ *     - 水浸:     water_infiltration_alarm_status = 1 → 严重告警
+ *     - 水位:     water_level_alarm_status = 1       → 严重告警
+ *     - 低电量:   battery_level <= 20               → 一般告警
+ *     - 弱信号:   signal_strength <= 20              → 一般告警
+ *
+ * 后端接口: POST /manhole/device/getAlarmMonitorList
+ * 请求参数: { keyword?: string }  // keyword 模糊匹配 equipment_code / equipment_name
+ * 返回结构:
+ * {
+ *   code: 200,
+ *   data: {
+ *     devices: [{
+ *       equipmentId: string,
+ *       equipmentCode: string,
+ *       equipmentName: string,
+ *       equipmentLocation: string,
+ *       equipmentTypeName: string,
+ *       manholeData: {                  // jg_device_data 最新一条记录
+ *         id: string,
+ *         batteryLevel: string,          // 电量百分比
+ *         signalStrength: string,        // 信号强度百分比
+ *         temperatureValue: string,      // 温度
+ *         tiltAngle: string,             // 当前倾斜角度
+ *         angleAlarmThreshold: string,   // 倾斜报警阈值
+ *         alarmStatus: string,           // 综合报警状态 0=正常 1=报警
+ *         waterInfiltrationAlarmStatus: string, // 水浸报警 0=正常 1=报警
+ *         waterLevelAlarmStatus: string,        // 水位报警 0=正常 1=报警
+ *         uploadTime: string,            // 数据上传时间
+ *         createTime: string             // 记录创建时间
+ *       },
+ *       equipmentStatus: {              // 设备运维状态
+ *         currentStatus: number,         // 1=在用 3=维修
+ *         alarmStatus: number,           // 0=正常 1=报警
+ *         onlineStatus: number           // 0=离线 1=在线
+ *       }
+ *     }],
+ *     statistics: {
+ *       total: number,                  // 设备总数
+ *       critical: number,               // 严重告警数
+ *       warning: number,                // 一般告警数
+ *       normal: number,                 // 正常设备数
+ *       onlineCount: number,            // 在线设备数
+ *       avgBattery: number              // 平均电量
+ *     }
+ *   }
+ * }
+ */
+export function getManholeAlarmMonitorList(data = {}) {
+  return request({ url: '/manhole/device/getAlarmMonitorList', method: 'post', data })
+}
+
 export function getManholeAlarmDeviceDetail(id) {
   return request({ url: '/manhole/device/getById/' + id, method: 'get' })
 }
@@ -119,6 +206,28 @@ export function addManholeWorkOrder(data) {
 export function operManholeWorkOrder(data) {
   return request({ url: '/manhole/workOrder/operWorkOrder', method: 'post', data })
 }
+// 修改设备运维状态(currentStatus:1-在用 2-闲置 3-维修 4-报废 5-待入库)
+export function updateManholeDeviceStatus(data) {
+  return request({ url: '/manhole/device/updateDeviceStatus', method: 'post', data })
+}
+
+// ============ 运维数据统计 ============
+// 运维看板统计数据:一次返回KPI指标/异常终端趋势/处置率延期率趋势/问题类型分布/区域排行/异常终端列表
+export function getManholeMaintenanceDashboard(data = {}) {
+  return request({ url: '/manhole/maintenanceDataStat/getDashboardStat', method: 'post', data })
+}
+// 根据日期查询运维数据统计(KPI指标)
+export function getMaintenanceDataStatByDate(data) {
+  return request({ url: '/manhole/maintenanceDataStat/queryByDate', method: 'post', data })
+}
+// 周度/月度 异常终端设备数量趋势分析
+export function getAbnormalTerminalTrend(data) {
+  return request({ url: '/manhole/maintenanceDataStat/abnormalTerminalTrend', method: 'post', data })
+}
+// 周度/月度 案件处置率/延期率趋势分析
+export function getCaseRateTrend(data) {
+  return request({ url: '/manhole/maintenanceDataStat/caseRateTrend', method: 'post', data })
+}
 
 // ============ 维修记录 ============
 export function getRepairRecordPage(pageNum, pageSize, params) {
@@ -343,3 +452,26 @@ export function getGisManholes() {
 export function getGisEquipments(params) {
   return request({ url: '/api/gis/equipments', method: 'get', params })
 }
+
+// ============ 预警阈值配置 ============
+export function getWarningThresholdPage(pageNum, pageSize, params) {
+  return request({ url: '/warningThreshold/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
+}
+export function getWarningThresholdModulePage(pageNum, pageSize, typeName, params) {
+  return request({ url: '/warningThreshold/findByModulePage', method: 'get', params: { pageNum, pageSize, typeName, ...params } })
+}
+export function getWarningThresholdList(params) {
+  return request({ url: '/warningThreshold/getWarningThresholdList', method: 'get', params })
+}
+export function getWarningThresholdById(id) {
+  return request({ url: '/warningThreshold/getById/' + id, method: 'get' })
+}
+export function saveWarningThreshold(data) {
+  return request({ url: '/warningThreshold/save', method: 'post', data })
+}
+export function updateWarningThreshold(data) {
+  return request({ url: '/warningThreshold/update', method: 'post', data })
+}
+export function deleteWarningThreshold(ids) {
+  return request({ url: '/warningThreshold/deleteBatch', method: 'post', data: ids })
+}

+ 329 - 0
src/views/subSystem/basic/GasThreshold.vue

@@ -0,0 +1,329 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索区 -->
+    <el-card class="search-card">
+      <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
+        <el-form-item label="设备编码">
+          <el-input v-model="queryParams.deviceCode" placeholder="请输入设备编码" clearable style="width: 180px" />
+        </el-form-item>
+        <el-form-item label="预警类型">
+          <el-select v-model="queryParams.warningType" placeholder="请选择" clearable style="width: 160px">
+            <el-option v-for="opt in WARNING_TYPE_OPTIONS" :key="opt.warningType" :label="opt.warningType" :value="opt.warningType" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="预警编码">
+          <el-input v-model="queryParams.warningCode" placeholder="请输入预警编码" clearable style="width: 180px" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleQuery">查询</el-button>
+          <el-button @click="resetQuery">重置</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" v-loading="loading" border style="width: 100%">
+        <el-table-column label="设备编码" prop="deviceCode" min-width="140" show-overflow-tooltip />
+        <el-table-column label="设备名称" min-width="140" show-overflow-tooltip>
+          <template #default="{ row }">
+            {{ getDeviceName(row.deviceCode) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="设备类型" min-width="120" show-overflow-tooltip>
+          <template #default="{ row }">
+            {{ getDeviceType(row.deviceCode) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="预警类型" prop="warningType" min-width="120" />
+        <el-table-column label="预警编码" prop="warningCode" min-width="140" />
+        <el-table-column label="最小值" prop="minValue" width="90" align="center" />
+        <el-table-column label="最大值" prop="maxValue" width="90" align="center" />
+        <el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
+        <el-table-column label="创建时间" prop="createTime" width="170" />
+        <el-table-column label="操作" width="160" align="center" 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]"
+        @size-change="handleSizeChange" @current-change="handlePageChange"
+        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" :rules="rules" ref="formRef" label-width="100px">
+        <el-form-item label="选择设备" prop="deviceCode">
+          <el-select v-model="formData.deviceCode" placeholder="请选择设备" filterable clearable style="width: 100%"
+            @click="loadGasDeviceOptions">
+            <el-option v-for="d in deviceOptions" :key="d.equipmentCode" :label="d.equipmentCode + ' - ' + d.equipmentName" :value="d.equipmentCode" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="预警类型" prop="warningType">
+          <el-select v-model="formData.warningType" placeholder="请选择预警类型" style="width: 100%" @change="onWarningTypeChange">
+            <el-option v-for="opt in WARNING_TYPE_OPTIONS" :key="opt.warningType" :label="opt.warningType" :value="opt.warningType" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="预警编码">
+          <el-input v-model="formData.warningCode" placeholder="选择预警类型后自动带出" />
+        </el-form-item>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="最小值" prop="minValue">
+              <el-input-number v-model="formData.minValue" :precision="2" :min="0" style="width: 100%" placeholder="阈值最小值" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="最大值" prop="maxValue">
+              <el-input-number v-model="formData.maxValue" :precision="2" :min="0" style="width: 100%" 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="handleSave" :loading="submitLoading">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="GasThreshold">
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getWarningThresholdModulePage, saveWarningThreshold, updateWarningThreshold, deleteWarningThreshold } from '@/api/pipeNetwork/basic'
+import request from '@/utils/request'
+
+// ==================== 燃气模块预警类型与预警编码参考 ====================
+const WARNING_TYPE_OPTIONS = [
+  { warningType: '可燃气体浓度预警', warningCode: 'WARN-GAS-CONCENTRATION' },
+  { warningType: '可燃气体温度预警', warningCode: 'WARN-GAS-TEMPERATURE' },
+  { warningType: '可燃气体湿度预警', warningCode: 'WARN-GAS-HUMIDITY' },
+  { warningType: '压力预警', warningCode: 'WARN-PRESSURE' },
+  { warningType: '温度预警', warningCode: 'WARN-TEMPERATURE' },
+  { warningType: '湿度预警', warningCode: 'WARN-HUMIDITY' }
+]
+
+// ==================== 搜索参数 ====================
+const queryParams = reactive({
+  deviceCode: '',
+  warningType: '',
+  warningCode: ''
+})
+const queryRef = ref(null)
+
+function handleQuery() {
+  pageNum.value = 1
+  loadData()
+}
+function resetQuery() {
+  queryParams.deviceCode = ''
+  queryParams.warningType = ''
+  queryParams.warningCode = ''
+  pageNum.value = 1
+  loadData()
+}
+
+// ==================== 表格数据 ====================
+const tableData = ref([])
+const total = ref(0)
+const loading = ref(false)
+const pageNum = ref(1)
+const pageSize = ref(10)
+
+async function loadData() {
+  loading.value = true
+  try {
+    const params = {}
+    if (queryParams.deviceCode) params.deviceCode = queryParams.deviceCode
+    if (queryParams.warningType) params.warningType = queryParams.warningType
+    if (queryParams.warningCode) params.warningCode = queryParams.warningCode
+    const res = await getWarningThresholdModulePage(pageNum.value, pageSize.value, '燃气', params)
+    const pageData = res.data || res
+    tableData.value = pageData.records || []
+    total.value = pageData.total || 0
+  } catch (e) {
+    console.error('加载阈值列表失败', e)
+    ElMessage.error('加载阈值列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+function handlePageChange() { loadData() }
+function handleSizeChange() { pageNum.value = 1; loadData() }
+
+// ==================== 设备名称/类型映射 ====================
+const deviceInfoMap = ref({})
+
+async function ensureDeviceMap() {
+  if (Object.keys(deviceInfoMap.value).length > 0) return
+  try {
+    const res = await request({ url: '/EquipmentBase/findByTopLevelType', method: 'get', params: { typeName: '燃气' } })
+    const list = res.data || []
+    const map = {}
+    list.forEach(d => { map[d.equipmentCode] = d })
+    deviceInfoMap.value = map
+  } catch (e) { console.error('加载燃气设备列表失败', e) }
+}
+
+function getDeviceName(deviceCode) {
+  const d = deviceInfoMap.value[deviceCode]
+  return d ? d.equipmentName : deviceCode
+}
+
+function getDeviceType(deviceCode) {
+  const d = deviceInfoMap.value[deviceCode]
+  return d ? (d.typeName || '-') : '-'
+}
+
+// ==================== 新增/编辑 ====================
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增阈值')
+const formRef = ref(null)
+const submitLoading = ref(false)
+const isEdit = ref(false)
+
+const rules = {
+  deviceCode: [{ required: true, message: '请选择设备', trigger: 'change' }],
+  warningType: [{ required: true, message: '请选择预警类型', trigger: 'change' }]
+}
+
+const formData = reactive({
+  id: '',
+  deviceCode: '',
+  warningType: '',
+  warningCode: '',
+  minValue: null,
+  maxValue: null,
+  remark: ''
+})
+
+const deviceOptions = ref([])
+
+async function loadGasDeviceOptions() {
+  if (deviceOptions.value.length > 0) return
+  try {
+    const res = await request({ url: '/EquipmentBase/findByTopLevelType', method: 'get', params: { typeName: '燃气' } })
+    deviceOptions.value = res.data || []
+  } catch (e) {
+    console.error('加载燃气设备列表失败', e)
+  }
+}
+
+function resetForm() {
+  formData.id = ''
+  formData.deviceCode = ''
+  formData.warningType = ''
+  formData.warningCode = ''
+  formData.minValue = null
+  formData.maxValue = null
+  formData.remark = ''
+  isEdit.value = false
+  dialogTitle.value = '新增阈值'
+}
+
+// 选择预警类型后自动带出预警编码
+function onWarningTypeChange(val) {
+  const opt = WARNING_TYPE_OPTIONS.find(o => o.warningType === val)
+  formData.warningCode = opt ? opt.warningCode : ''
+}
+
+async function handleAdd() {
+  resetForm()
+  await loadGasDeviceOptions()
+  dialogVisible.value = true
+}
+
+function handleEdit(row) {
+  isEdit.value = true
+  dialogTitle.value = '编辑阈值'
+  formData.id = row.id || ''
+  formData.deviceCode = row.deviceCode
+  formData.warningType = row.warningType
+  formData.warningCode = row.warningCode || ''
+  formData.minValue = row.minValue
+  formData.maxValue = row.maxValue
+  formData.remark = row.remark || ''
+  dialogVisible.value = true
+}
+
+async function handleSave() {
+  const valid = await formRef.value.validate().catch(() => false)
+  if (!valid) return
+  submitLoading.value = true
+  try {
+    const data = {
+      deviceCode: formData.deviceCode,
+      warningType: formData.warningType,
+      warningCode: formData.warningCode || undefined,
+      minValue: formData.minValue,
+      maxValue: formData.maxValue,
+      remark: formData.remark || undefined
+    }
+    if (isEdit.value) {
+      data.id = formData.id
+      await updateWarningThreshold(data)
+      ElMessage.success('修改成功')
+    } else {
+      await saveWarningThreshold(data)
+      ElMessage.success('新增成功')
+    }
+    dialogVisible.value = false
+    loadData()
+    // 刷新设备映射
+    deviceInfoMap.value = {}
+    await ensureDeviceMap()
+  } catch (e) {
+    const msg = e?.response?.data?.msg || e?.message || '操作失败'
+    ElMessage.error(msg)
+  } finally {
+    submitLoading.value = false
+  }
+}
+
+// ==================== 删除 ====================
+function handleDelete(row) {
+  ElMessageBox.confirm('确认删除该阈值配置?', '提示', { type: 'warning' }).then(async () => {
+    try {
+      await deleteWarningThreshold([row.id])
+      ElMessage.success('删除成功')
+      loadData()
+    } catch (e) {
+      ElMessage.error(e?.response?.data?.msg || '删除失败')
+    }
+  }).catch(() => {})
+}
+
+// ==================== 生命周期 ====================
+onMounted(async () => {
+  await ensureDeviceMap()
+  loadData()
+})
+</script>
+
+<style scoped>
+.app-container {
+  padding: 16px;
+}
+.search-card {
+  margin-bottom: 16px;
+}
+.search-form {
+  display: flex;
+  flex-wrap: wrap;
+}
+.table-card {
+  min-height: 400px;
+}
+</style>

+ 1 - 1
src/views/subSystem/drainage/jcsj/yjsdgl.vue

@@ -37,7 +37,7 @@
       <el-table-column label="易积水点数量" prop="waterloggingCount" min-width="100" />
       <el-table-column label="积水量" prop="waterVolume" min-width="100" />
       <el-table-column label="积水程度" min-width="120">
-        <template #default="{ row }">D:\work1\city-life-line\src\views\subSystem\drainage\DrainageWorkOrder.vue
+        <template #default="{ row }">
           <el-tag :type="getWaterloggingLevelType(row.waterloggingLevel)" size="small">
             {{ getWaterloggingLevelLabel(row.waterloggingLevel) }}
           </el-tag>

+ 340 - 0
src/views/subSystem/drainage/jcyj/DrainageThreshold.vue

@@ -0,0 +1,340 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索区 -->
+    <el-card class="search-card">
+      <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
+        <el-form-item label="设备编码">
+          <el-input v-model="queryParams.deviceCode" placeholder="请输入设备编码" clearable style="width: 180px" />
+        </el-form-item>
+        <el-form-item label="预警类型">
+          <el-select v-model="queryParams.warningType" placeholder="请选择" clearable style="width: 160px">
+            <el-option v-for="opt in WARNING_TYPE_OPTIONS" :key="opt.warningType" :label="opt.warningType" :value="opt.warningType" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="预警编码">
+          <el-input v-model="queryParams.warningCode" placeholder="请输入预警编码" clearable style="width: 180px" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleQuery">查询</el-button>
+          <el-button @click="resetQuery">重置</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" v-loading="loading" border style="width: 100%">
+        <el-table-column label="设备编码" prop="deviceCode" min-width="140" show-overflow-tooltip />
+        <el-table-column label="设备名称" min-width="140" show-overflow-tooltip>
+          <template #default="{ row }">
+            {{ getDeviceName(row.deviceCode) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="设备类型" min-width="120" show-overflow-tooltip>
+          <template #default="{ row }">
+            {{ getDeviceType(row.deviceCode) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="预警类型" prop="warningType" min-width="120" />
+        <el-table-column label="预警编码" prop="warningCode" min-width="140" />
+        <el-table-column label="最小值" prop="minValue" width="90" align="center" />
+        <el-table-column label="最大值" prop="maxValue" width="90" align="center" />
+        <el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
+        <el-table-column label="创建时间" prop="createTime" width="170" />
+        <el-table-column label="操作" width="160" align="center" 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]"
+        @size-change="handleSizeChange" @current-change="handlePageChange"
+        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" :rules="rules" ref="formRef" label-width="100px">
+        <el-form-item label="选择设备" prop="deviceCode">
+          <el-select v-model="formData.deviceCode" placeholder="请选择设备" filterable clearable style="width: 100%"
+            @click="loadDrainageDeviceOptions">
+            <el-option v-for="d in deviceOptions" :key="d.equipmentCode"
+              :label="(d.equipmentCode || d.deviceCode) + ' - ' + (d.equipmentName || d.deviceName)"
+              :value="d.equipmentCode || d.deviceCode" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="预警类型" prop="warningType">
+          <el-select v-model="formData.warningType" placeholder="请选择预警类型" style="width: 100%" @change="onWarningTypeChange">
+            <el-option v-for="opt in WARNING_TYPE_OPTIONS" :key="opt.warningType" :label="opt.warningType" :value="opt.warningType" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="预警编码">
+          <el-input v-model="formData.warningCode" placeholder="选择预警类型后自动带出" />
+        </el-form-item>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="最小值" prop="minValue">
+              <el-input-number v-model="formData.minValue" :precision="2" :min="0" style="width: 100%" placeholder="阈值最小值" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="最大值" prop="maxValue">
+              <el-input-number v-model="formData.maxValue" :precision="2" :min="0" style="width: 100%" 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="handleSave" :loading="submitLoading">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="DrainageThreshold">
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+  getThresholdModulePage, saveThreshold, updateThreshold, deleteThreshold,
+  getEquipmentPage
+} from '@/api/drainage'
+import request from '@/utils/request'
+
+// ==================== 排水模块预警类型与预警编码参考 ====================
+const WARNING_TYPE_OPTIONS = [
+  { warningType: '水位预警', warningCode: 'WARN-WATER-LEVEL' },
+  { warningType: '流速预警', warningCode: 'WARN-FLOW-SPEED' },
+  { warningType: '悬浮物预警', warningCode: 'WARN-SUSPENDED-SOLIDS' },
+  { warningType: '氨氮预警', warningCode: 'WARN-AMMONIA-NITROGEN' },
+  { warningType: '电导率预警', warningCode: 'WARN-CONDUCTIVITY' },
+  { warningType: 'PH预警', warningCode: 'WARN-PH' },
+  { warningType: '瞬时流量预警', warningCode: 'WARN_INSTANT_FLOW' },
+  { warningType: '倾斜预警', warningCode: 'WARN-TILT' },
+  { warningType: '温度预警', warningCode: 'WARN-TEMPERATURE' },
+  { warningType: '湿度预警', warningCode: 'WARN-HUMIDITY' }
+]
+
+// ==================== 搜索参数 ====================
+const queryParams = reactive({
+  deviceCode: '',
+  warningType: '',
+  warningCode: ''
+})
+const queryRef = ref(null)
+
+function handleQuery() {
+  pageNum.value = 1
+  loadData()
+}
+function resetQuery() {
+  queryParams.deviceCode = ''
+  queryParams.warningType = ''
+  queryParams.warningCode = ''
+  pageNum.value = 1
+  loadData()
+}
+
+// ==================== 表格数据 ====================
+const tableData = ref([])
+const total = ref(0)
+const loading = ref(false)
+const pageNum = ref(1)
+const pageSize = ref(10)
+
+async function loadData() {
+  loading.value = true
+  try {
+    const params = {}
+    if (queryParams.deviceCode) params.deviceCode = queryParams.deviceCode
+    if (queryParams.warningType) params.warningType = queryParams.warningType
+    if (queryParams.warningCode) params.warningCode = queryParams.warningCode
+    const res = await getThresholdModulePage(pageNum.value, pageSize.value, '排水', params)
+    const pageData = res.data || res
+    tableData.value = pageData.records || []
+    total.value = pageData.total || 0
+  } catch (e) {
+    console.error('加载阈值列表失败', e)
+    ElMessage.error('加载阈值列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+function handlePageChange() { loadData() }
+function handleSizeChange() { pageNum.value = 1; loadData() }
+
+// ==================== 设备名称/类型映射 ====================
+const deviceInfoMap = ref({})
+
+async function ensureDeviceMap() {
+  if (Object.keys(deviceInfoMap.value).length > 0) return
+  try {
+    const res = await request({ url: '/EquipmentBase/findByTopLevelType', method: 'get', params: { typeName: '排水' } })
+    const list = res.data || []
+    const map = {}
+    list.forEach(d => { map[d.equipmentCode] = d })
+    deviceInfoMap.value = map
+  } catch (e) { console.error('加载排水设备列表失败', e) }
+}
+
+function getDeviceName(deviceCode) {
+  const d = deviceInfoMap.value[deviceCode]
+  return d ? d.equipmentName : deviceCode
+}
+
+function getDeviceType(deviceCode) {
+  const d = deviceInfoMap.value[deviceCode]
+  return d ? (d.typeName || '-') : '-'
+}
+
+// ==================== 新增/编辑 ====================
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增阈值')
+const formRef = ref(null)
+const submitLoading = ref(false)
+const isEdit = ref(false)
+
+const rules = {
+  deviceCode: [{ required: true, message: '请选择设备', trigger: 'change' }],
+  warningType: [{ required: true, message: '请选择预警类型', trigger: 'change' }]
+}
+
+const formData = reactive({
+  id: '',
+  deviceCode: '',
+  warningType: '',
+  warningCode: '',
+  minValue: null,
+  maxValue: null,
+  remark: ''
+})
+
+const deviceOptions = ref([])
+
+async function loadDrainageDeviceOptions() {
+  if (deviceOptions.value.length > 0) return
+  try {
+    // 优先使用排水设备专用接口
+    const res = await getEquipmentPage(1, 200, {})
+    const pageData = res.data || res
+    deviceOptions.value = pageData.records || pageData.rows || []
+  } catch (e) {
+    console.error('加载排水设备列表失败', e)
+  }
+}
+
+function resetForm() {
+  formData.id = ''
+  formData.deviceCode = ''
+  formData.warningType = ''
+  formData.warningCode = ''
+  formData.minValue = null
+  formData.maxValue = null
+  formData.remark = ''
+  isEdit.value = false
+  dialogTitle.value = '新增阈值'
+}
+
+// 选择预警类型后自动带出预警编码
+function onWarningTypeChange(val) {
+  const opt = WARNING_TYPE_OPTIONS.find(o => o.warningType === val)
+  formData.warningCode = opt ? opt.warningCode : ''
+}
+
+async function handleAdd() {
+  resetForm()
+  await loadDrainageDeviceOptions()
+  dialogVisible.value = true
+}
+
+function handleEdit(row) {
+  isEdit.value = true
+  dialogTitle.value = '编辑阈值'
+  formData.id = row.id || ''
+  formData.deviceCode = row.deviceCode
+  formData.warningType = row.warningType
+  formData.warningCode = row.warningCode || ''
+  formData.minValue = row.minValue
+  formData.maxValue = row.maxValue
+  formData.remark = row.remark || ''
+  dialogVisible.value = true
+}
+
+async function handleSave() {
+  const valid = await formRef.value.validate().catch(() => false)
+  if (!valid) return
+  submitLoading.value = true
+  try {
+    const data = {
+      deviceCode: formData.deviceCode,
+      warningType: formData.warningType,
+      warningCode: formData.warningCode || undefined,
+      minValue: formData.minValue,
+      maxValue: formData.maxValue,
+      remark: formData.remark || undefined
+    }
+    if (isEdit.value) {
+      data.id = formData.id
+      await updateThreshold(data)
+      ElMessage.success('修改成功')
+    } else {
+      await saveThreshold(data)
+      ElMessage.success('新增成功')
+    }
+    dialogVisible.value = false
+    loadData()
+    // 刷新设备映射
+    deviceInfoMap.value = {}
+    await ensureDeviceMap()
+  } catch (e) {
+    const msg = e?.response?.data?.msg || e?.message || '操作失败'
+    ElMessage.error(msg)
+  } finally {
+    submitLoading.value = false
+  }
+}
+
+// ==================== 删除 ====================
+function handleDelete(row) {
+  ElMessageBox.confirm('确认删除该阈值配置?', '提示', { type: 'warning' }).then(async () => {
+    try {
+      await deleteThreshold([row.id])
+      ElMessage.success('删除成功')
+      loadData()
+    } catch (e) {
+      ElMessage.error(e?.response?.data?.msg || '删除失败')
+    }
+  }).catch(() => {})
+}
+
+// ==================== 生命周期 ====================
+onMounted(async () => {
+  await ensureDeviceMap()
+  loadData()
+})
+</script>
+
+<style scoped>
+.app-container {
+  padding: 16px;
+}
+.search-card {
+  margin-bottom: 16px;
+}
+.search-form {
+  display: flex;
+  flex-wrap: wrap;
+}
+.table-card {
+  min-height: 400px;
+}
+</style>

+ 138 - 43
src/views/subSystem/manholeCover/alarm/alarmList.vue

@@ -27,11 +27,11 @@
       <div class="alert-list-panel" v-loading="listLoading">
         <div class="panel-header">
           <span>报警事件列表 <el-badge :value="unhandledCount" class="badge" /></span>
-          <el-input v-model="searchText" placeholder="搜索位置/设备编号" prefix-icon="Search" clearable size="small" style="width: 180px" />
+          <el-input v-model="searchText" placeholder="搜索位置/设备编号" prefix-icon="Search" clearable size="small" style="width: 180px" @input="handleSearch" />
         </div>
         <div class="alert-list">
           <div
-              v-for="alert in filteredAlerts"
+              v-for="alert in alerts"
               :key="alert.id"
               class="alert-item"
               :class="{
@@ -67,7 +67,18 @@
               <el-tag type="danger" size="small">待处理</el-tag>
             </div>
           </div>
-          <el-empty v-if="!filteredAlerts.length && !listLoading" description="暂无报警事件" :image-size="100" />
+          <el-empty v-if="!alerts.length && !listLoading" description="暂无报警事件" :image-size="100" />
+        </div>
+        <div class="pagination-area">
+          <el-pagination
+            small
+            background
+            layout="total, prev, pager, next"
+            :total="total"
+            :page-size="pageSize"
+            v-model:current-page="currentPage"
+            @current-change="loadAlerts"
+          />
         </div>
       </div>
 
@@ -78,7 +89,7 @@
           <div class="section-title">
             <span><el-icon><Warning /></el-icon> 报警详情 - 辅助研判</span>
           </div>
-          <el-descriptions :column="2" border>
+          <el-descriptions :column="2" border class="detail-desc">
             <el-descriptions-item label="报警位置">
               <strong>{{ selectedAlert.location }}</strong> <el-button link type="primary" size="small" @click="viewOnMap">查看地图</el-button>
             </el-descriptions-item>
@@ -111,7 +122,7 @@
     </div>
 
     <!-- 地图弹窗 -->
-    <el-dialog v-model="mapDialogVisible" title="报警位置地图" width="720px" @opened="handleMapDialogOpened">
+    <el-dialog v-model="mapDialogVisible" title="报警位置地图" width="80%" class="map-dialog" @opened="handleMapDialogOpened">
       <div class="dialog-map-wrapper">
         <div ref="dialogMapRef" class="dialog-baidu-map"></div>
         <div v-if="dialogMapError" class="dialog-map-error">{{ dialogMapError }}</div>
@@ -134,8 +145,12 @@ import {ElMessage} from 'element-plus'
 import {getManholeAlarmDeviceDetail, getManholeAlarmDeviceList} from '@/api/pipeNetwork/basic'
 
 const alerts = ref([])
+const allAlerts = ref([])
 const searchText = ref('')
 const filterType = ref('all')
+const currentPage = ref(1)
+const pageSize = 6
+const total = ref(0)
 const selectedAlert = ref(null)
 const mapDialogVisible = ref(false)
 const listLoading = ref(false)
@@ -143,6 +158,17 @@ const detailLoading = ref(false)
 const dialogMapRef = ref(null)
 const dialogMapError = ref('')
 
+// 搜索防抖
+let searchTimer = null
+function handleSearch() {
+  clearTimeout(searchTimer)
+  searchTimer = setTimeout(() => {
+    currentPage.value = 1
+    loadAlerts()
+    loadStats()
+  }, 300)
+}
+
 const BAIDU_MAP_AK = import.meta.env.VITE_BAIDU_MAP_AK || ''
 const DEFAULT_MAP_CENTER = { lng: 110.393, lat: 28.452 }
 
@@ -281,14 +307,21 @@ function normalizeAlert(raw, baseAlert = null) {
   }
 }
 
+// 报警级别映射:全部=0,严重=1,一般=2
+const ALARM_LEVEL_MAP = { all: 0, critical: 1, warning: 2 }
+
 async function loadAlerts() {
   listLoading.value = true
   try {
     const res = await getManholeAlarmDeviceList({
-      alarmStatus: 1,
-      isQueryManholeData: true
+      keyword: searchText.value.trim() || undefined,
+      alarmLevel: ALARM_LEVEL_MAP[filterType.value],
+      isQueryManholeData: true,
+      pageNum: currentPage.value,
+      pageSize
     })
-    const list = Array.isArray(res?.data) ? res.data : []
+    const list = res.data?.records || []
+    total.value = res.data?.total || list.length
     alerts.value = list
       .map(item => normalizeAlert(item))
       .filter(Boolean)
@@ -310,6 +343,25 @@ async function loadAlerts() {
   }
 }
 
+// 加载全量报警数据(用于顶部统计卡片,不受分页影响)
+async function loadStats() {
+  try {
+    const res = await getManholeAlarmDeviceList({
+      keyword: searchText.value.trim() || undefined,
+      alarmLevel: 0,
+      isQueryManholeData: true,
+      pageNum: 1,
+      pageSize: 1000
+    })
+    const list = res.data?.records || []
+    allAlerts.value = list
+      .map(item => normalizeAlert(item))
+      .filter(Boolean)
+  } catch (error) {
+    allAlerts.value = []
+  }
+}
+
 async function loadAlertDetail(alert) {
   if (!alert?.equipmentId) return
   detailLoading.value = true
@@ -332,33 +384,17 @@ async function loadAlertDetail(alert) {
   }
 }
 
-const filteredAlerts = computed(() => {
-  let list = alerts.value
-  if (filterType.value === 'critical') {
-    list = list.filter(a => a.level === 'critical')
-  } else if (filterType.value === 'warning') {
-    list = list.filter(a => a.level === 'warning')
-  }
-
-  if (searchText.value) {
-    const kw = searchText.value.toLowerCase()
-    list = list.filter(a =>
-      `${a.location || ''}`.toLowerCase().includes(kw) ||
-      `${a.deviceCode || ''}`.toLowerCase().includes(kw) ||
-      `${a.deviceName || ''}`.toLowerCase().includes(kw)
-    )
-  }
-  return list
-})
-
-const totalAlerts = computed(() => alerts.value.length)
-const criticalCount = computed(() => alerts.value.filter(a => a.level === 'critical').length)
-const warningCount = computed(() => alerts.value.filter(a => a.level === 'warning').length)
-const unhandledCount = computed(() => alerts.value.length)
+const totalAlerts = computed(() => allAlerts.value.length)
+const criticalCount = computed(() => allAlerts.value.filter(a => a.level === 'critical').length)
+const warningCount = computed(() => allAlerts.value.filter(a => a.level === 'warning').length)
+const unhandledCount = computed(() => allAlerts.value.length)
 const hasAlertCoordinate = computed(() => isValidCoordinate(toNumber(selectedAlert.value?.lng), toNumber(selectedAlert.value?.lat)))
 
 const filterAlerts = (type) => {
+  if (filterType.value === type) return
   filterType.value = type
+  currentPage.value = 1
+  loadAlerts()
 }
 
 const selectAlert = async (alert) => {
@@ -530,10 +566,11 @@ watch(
 )
 
 onMounted(async () => {
-  await loadAlerts()
+  await Promise.all([loadAlerts(), loadStats()])
 })
 
 onUnmounted(() => {
+  clearTimeout(searchTimer)
   clearDialogMapMarker()
   dialogMapInstance = null
 })
@@ -607,16 +644,37 @@ onUnmounted(() => {
 .exceed { color: #f56c6c; }
 .detail-panel {
   flex: 1;
+  min-width: 0;
   background: white;
   border-radius: 20px;
   padding: 20px;
   overflow-y: auto;
   max-height: calc(100vh - 150px);
 }
-.detail-section {
-  margin-bottom: 24px;
-  border-bottom: 1px solid #f0f0f0;
-  padding-bottom: 20px;
+.action-buttons { display: flex; gap: 8px; }
+.empty-detail { flex: 1; min-width: 0; display: flex; align-items: center; justify-content: center; background: white; border-radius: 20px; }
+
+/* ==================== 字体放大与自适应 ==================== */
+/* 详情区字体放大 */
+.detail-desc :deep(.el-descriptions__label) {
+  font-size: 14px;
+}
+.detail-desc :deep(.el-descriptions__content) {
+  font-size: 15px;
+}
+.detail-desc :deep(.el-descriptions__content strong) {
+  font-size: 16px;
+}
+.detail-desc .warning-value {
+  color: #f56c6c;
+  font-size: 22px;
+  margin: 0 4px;
+}
+.detail-desc .real-time-data {
+  background: #fff9f0;
+  padding: 8px;
+  border-radius: 8px;
+  font-size: 15px;
 }
 .section-title {
   display: flex;
@@ -624,24 +682,48 @@ onUnmounted(() => {
   align-items: center;
   margin-bottom: 16px;
   font-weight: 600;
-  font-size: 16px;
+  font-size: 18px;
 }
-.real-time-data { background: #fff9f0; padding: 8px; border-radius: 8px; }
-.warning-value { color: #f56c6c; font-size: 18px; margin: 0 4px; }
-.action-buttons { display: flex; gap: 8px; }
-.empty-detail { flex: 1; display: flex; align-items: center; justify-content: center; background: white; border-radius: 20px; }
+.detail-section {
+  margin-bottom: 24px;
+  border-bottom: 1px solid #f0f0f0;
+  padding-bottom: 20px;
+}
+
+/* 自适应:窄屏时左侧列表收窄,详情不被挤压 */
+@media (max-width: 1500px) {
+  .alert-list-panel {
+    width: 340px;
+  }
+  .detail-desc :deep(.el-descriptions) {
+    --el-descriptions-item-bordered-label-background: #f5f7fa;
+  }
+}
+@media (max-width: 1200px) {
+  .main-content {
+    flex-direction: column;
+  }
+  .alert-list-panel {
+    width: 100%;
+  }
+  .alert-list {
+    max-height: 360px;
+  }
+}
+/* ==================== 地图弹窗 ==================== */
 .dialog-map-wrapper {
   position: relative;
 }
+/* 地图高度放大 */
 .dialog-baidu-map {
-  height: 420px;
+  height: 520px;
   border-radius: 12px;
   overflow: hidden;
   background: #eef2f0;
 }
 .dialog-map-footer {
   padding-top: 12px;
-  font-size: 13px;
+  font-size: 14px;
   color: #606266;
   line-height: 1.8;
 }
@@ -671,4 +753,17 @@ onUnmounted(() => {
   line-height: 1.6;
   z-index: 2;
 }
+/* 弹窗宽度自适应(放大) */
+.map-dialog {
+  max-width: 1200px;
+}
+.map-dialog :deep(.el-dialog__body) {
+  padding: 16px 20px 20px;
+}
+
+.pagination-area {
+  padding-top: 10px;
+  display: flex;
+  justify-content: center;
+}
 </style>

+ 157 - 94
src/views/subSystem/manholeCover/alarm/alarmSanalysis.vue

@@ -50,16 +50,18 @@
         </div>
         <el-tree
             ref="layerTreeRef"
-            :data="layerTree"
+            :data="layerTreeData"
             show-checkbox
+            check-strictly
             node-key="id"
-            :default-checked-keys="defaultCheckedLayers"
+            :default-checked-keys="checkedKeys"
+            :default-expand-all="true"
             @check="onLayerChange"
             class="layer-tree"
         >
           <template #default="{ node, data }">
             <span class="layer-node">
-              <el-icon><component :is="data.icon" /></el-icon>
+              <el-icon v-if="data.icon"><component :is="data.icon" /></el-icon>
               <span>{{ node.label }}</span>
               <el-badge v-if="data.alertCount" :value="data.alertCount" type="danger" class="layer-badge" />
             </span>
@@ -153,43 +155,78 @@ import {
   ZoomOut
 } from '@element-plus/icons-vue'
 import {ElMessage} from 'element-plus'
-import {getManholeAlarmDeviceDetail, getManholeAlarmDeviceList} from '@/api/pipeNetwork/basic'
-
-const layerTree = ref([
-  {
-    id: 'current-status',
-    label: '运维状态筛选',
-    icon: Setting,
-    alertCount: 0,
-    children: [
-      { id: 'current-1', label: '在用设备', icon: Monitor, alertCount: 0 },
-      { id: 'current-3', label: '维修设备', icon: Setting, alertCount: 0 }
-    ]
-  },
-  {
-    id: 'alarm-status',
-    label: '报警状态筛选',
-    icon: WarningFilled,
-    alertCount: 0,
-    children: [
-      { id: 'alarm-1', label: '报警设备', icon: WarningFilled, alertCount: 0 },
-      { id: 'alarm-0', label: '正常设备', icon: InfoFilled, alertCount: 0 }
-    ]
-  },
-  {
-    id: 'online-status',
-    label: '在线状态筛选',
-    icon: Connection,
-    alertCount: 0,
-    children: [
-      { id: 'online-1', label: '在线设备', icon: Connection, alertCount: 0 },
-      { id: 'online-0', label: '离线设备', icon: Connection, alertCount: 0 }
-    ]
-  }
-])
-const defaultCheckedLayers = ['current-1', 'current-3', 'alarm-1', 'alarm-0', 'online-1', 'online-0']
-const visibleLayers = ref(new Set(defaultCheckedLayers))
+import {getManholeAlarmDeviceDetail, getManholeAlarmDeviceList, getManholeLayerData} from '@/api/pipeNetwork/basic'
+
+// 树复选框默认勾选全部状态
+const DEFAULT_CHECKED_KEYS = ['current-1', 'current-3', 'alarm-1', 'alarm-0', 'online-1', 'online-0']
+const checkedKeys = ref([...DEFAULT_CHECKED_KEYS])
+const visibleLayers = ref(new Set(DEFAULT_CHECKED_KEYS))
 const layerTreeRef = ref(null)
+
+// 动态生成树数据(包括设备级子节点),每次 displayDevices 变化自动重算
+const layerTreeData = computed(() => {
+  const devices = displayDevices.value
+  const total = devices.length
+  const critical = devices.filter(m => m.status === 'critical').length
+  const warning = devices.filter(m => m.status === 'warning').length
+  const normal = devices.filter(m => m.status === 'normal').length
+  const offline = devices.filter(m => m.status === 'offline').length
+  const inUse = devices.filter(item => Number(item.raw?.equipmentStatus?.currentStatus) === 1).length
+  const maintenance = devices.filter(item => Number(item.raw?.equipmentStatus?.currentStatus) === 3).length
+
+  function buildDeviceNode(device) {
+    const typeId = Number(device.raw?.equipmentTypeId)
+    const typeName = typeId === 5 ? '供水窨井' : typeId === 9 ? '排水窨井' : typeId === 10 ? '燃气窨井' : (device.equipmentTypeName || '未知类型')
+    const code = device.deviceCode || device.id || ''
+    return {
+      id: `device-${device.equipmentId}`,
+      label: `${typeName}-${code}`,
+      disabled: true
+    }
+  }
+
+  return [
+    {
+      id: 'current-status',
+      label: '运维状态筛选',
+      icon: Setting,
+      alertCount: inUse + maintenance,
+      disabled: true,
+      children: [
+        { id: 'current-1', label: '在用设备', icon: Monitor, alertCount: inUse,
+          children: devices.filter(d => Number(d.raw?.equipmentStatus?.currentStatus) === 1).map(buildDeviceNode) },
+        { id: 'current-3', label: '维修设备', icon: Setting, alertCount: maintenance,
+          children: devices.filter(d => Number(d.raw?.equipmentStatus?.currentStatus) === 3).map(buildDeviceNode) }
+      ]
+    },
+    {
+      id: 'alarm-status',
+      label: '报警状态筛选',
+      icon: WarningFilled,
+      alertCount: critical + warning + normal,
+      disabled: true,
+      children: [
+        { id: 'alarm-1', label: '报警设备', icon: WarningFilled, alertCount: critical + warning,
+          children: devices.filter(d => d.status === 'critical' || d.status === 'warning').map(buildDeviceNode) },
+        { id: 'alarm-0', label: '正常设备', icon: InfoFilled, alertCount: normal,
+          children: devices.filter(d => d.status === 'normal').map(buildDeviceNode) }
+      ]
+    },
+    {
+      id: 'online-status',
+      label: '在线状态筛选',
+      icon: Connection,
+      alertCount: total,
+      disabled: true,
+      children: [
+        { id: 'online-1', label: '在线设备', icon: Connection, alertCount: total - offline,
+          children: devices.filter(d => d.status !== 'offline').map(buildDeviceNode) },
+        { id: 'online-0', label: '离线设备', icon: Connection, alertCount: offline,
+          children: devices.filter(d => d.status === 'offline').map(buildDeviceNode) }
+      ]
+    }
+  ]
+})
 const queryParams = ref({
   keyword: ''
 })
@@ -226,21 +263,47 @@ function matchesQuickFilter(device) {
   return device.status === activeQuickFilter.value
 }
 
-function getSingleCheckedValue(checkedKeys, prefix) {
-  const matched = [...checkedKeys].filter(key => key.startsWith(prefix))
-  if (matched.length !== 1) return undefined
-  const value = Number(matched[0].slice(prefix.length))
-  return Number.isFinite(value) ? value : undefined
+/**
+ * 图层树勾选过滤:根据 visibleLayers 判断设备是否应显示
+ * - 无状态记录的设备视为离线(onlineStatus=0)
+ * - 某分类全部勾选时等同于不过滤(兼容无状态记录的设备)
+ */
+function matchesLayerFilter(device) {
+  const keys = visibleLayers.value
+  // 没有勾选任何状态时显示全部
+  if (keys.size === 0) return true
+
+  const st = device.raw?.equipmentStatus || {}
+  const currentStatus = Number(st.currentStatus)
+  const alarmStatus = Number(st.alarmStatus)
+  const onlineStatus = Number(st.onlineStatus)
+  const hasNoStatus = !device.raw?.equipmentStatus
+
+  // OR 逻辑:匹配任意一个勾选状态即显示
+  if (keys.has('current-1') && currentStatus === 1) return true
+  if (keys.has('current-3') && currentStatus === 3) return true
+  if (keys.has('alarm-1') && alarmStatus === 1) return true
+  if (keys.has('alarm-0') && alarmStatus === 0) return true
+  if (keys.has('online-1') && onlineStatus === 1) return true
+  if (keys.has('online-0') && (onlineStatus === 0 || hasNoStatus)) return true
+
+  return false
 }
 
 const displayDevices = computed(() => {
-  return mockManholes.value.filter(matchesQuickFilter)
+  return mockManholes.value.filter(device => {
+    // 图层树过滤
+    if (!matchesLayerFilter(device)) return false
+    // 顶部统计栏快速过滤
+    if (!matchesQuickFilter(device)) return false
+    return true
+  })
 })
 
-const criticalCount = computed(() => mockManholes.value.filter(m => m.status === 'critical').length)
-const warningCount = computed(() => mockManholes.value.filter(m => m.status === 'warning').length)
-const normalCount = computed(() => mockManholes.value.filter(m => m.status === 'normal').length)
-const offlineCount = computed(() => mockManholes.value.filter(m => m.status === 'offline').length)
+const criticalCount = computed(() => displayDevices.value.filter(m => m.status === 'critical').length)
+const warningCount = computed(() => displayDevices.value.filter(m => m.status === 'warning').length)
+const normalCount = computed(() => displayDevices.value.filter(m => m.status === 'normal').length)
+const offlineCount = computed(() => displayDevices.value.filter(m => m.status === 'offline').length)
 const totalDevices = computed(() => mockManholes.value.length)
 const totalActiveAlerts = computed(() => criticalCount.value + warningCount.value)
 
@@ -384,23 +447,7 @@ function normalizeDevice(raw, _index = 0, baseDevice = {}) {
 }
 
 function refreshLayerTree() {
-  const sourceDevices = mockManholes.value
-  const critical = criticalCount.value
-  const warning = warningCount.value
-  const normal = normalCount.value
-  const offline = offlineCount.value
-  const inUse = sourceDevices.filter(item => Number(item.raw?.equipmentStatus?.currentStatus) === 1).length
-  const maintenance = sourceDevices.filter(item => Number(item.raw?.equipmentStatus?.currentStatus) === 3).length
-
-  layerTree.value[0].alertCount = inUse + maintenance
-  layerTree.value[0].children[0].alertCount = inUse
-  layerTree.value[0].children[1].alertCount = maintenance
-  layerTree.value[1].alertCount = critical + warning + normal
-  layerTree.value[1].children[0].alertCount = critical + warning
-  layerTree.value[1].children[1].alertCount = normal
-  layerTree.value[2].alertCount = totalDevices.value
-  layerTree.value[2].children[0].alertCount = totalDevices.value - offline
-  layerTree.value[2].children[1].alertCount = offline
+  // 已废弃:树数据由 layerTreeData computed 自动生成
 }
 
 function refreshBounds() {
@@ -482,10 +529,14 @@ function fitMapViewport() {
 
 function renderMapOverlays() {
   if (!mapInstance || !window.BMapGL) return
+  console.log('[renderMapOverlays] 准备渲染的设备:', displayDevices.value.length)
   clearMapOverlays()
-  displayDevices.value
-    .filter(device => isValidCoordinate(device.lng, device.lat))
-    .forEach((device) => {
+  const validDevices = displayDevices.value.filter(device => isValidCoordinate(device.lng, device.lat))
+  console.log('[renderMapOverlays] 有效坐标设备:', validDevices.length)
+  console.log('[renderMapOverlays] 无效坐标设备:', displayDevices.value.filter(d => !isValidCoordinate(d.lng, d.lat)).map(d => ({ id: d.id, lng: d.lng, lat: d.lat })))
+  
+  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 isActive = selectedDevice.value?.equipmentId === device.equipmentId
     const label = new window.BMapGL.Label(getMarkerHtml(device, isActive), {
@@ -503,13 +554,18 @@ function renderMapOverlays() {
     })
     mapInstance.addOverlay(label)
     mapOverlays.push(label)
-    })
+  })
   fitMapViewport()
 }
 
+let _renderTimer = null
 function renderMap() {
   if (!mapInstance || !window.BMapGL) return
-  renderMapOverlays()
+  if (_renderTimer) clearTimeout(_renderTimer)
+  _renderTimer = setTimeout(() => {
+    renderMapOverlays()
+    _renderTimer = null
+  }, 50)
 }
 
 function ensureBaiduMap() {
@@ -590,26 +646,27 @@ async function loadDeviceList() {
   listLoading.value = true
   try {
     const keyword = queryParams.value.keyword?.trim()
-    const checkedKeys = visibleLayers.value
-    const currentStatus = getSingleCheckedValue(checkedKeys, 'current-')
-    const alarmStatus = getSingleCheckedValue(checkedKeys, 'alarm-')
-    const onlineStatus = getSingleCheckedValue(checkedKeys, 'online-')
-    const res = await getManholeAlarmDeviceList({
+    
+    // 只传 keyword 和 isQueryManholeData,不传状态筛选
+    // 树勾选过滤在 displayDevices computed 中做前端过滤
+    const res = await getManholeLayerData({
       keyword: keyword || undefined,
-      currentStatus,
-      alarmStatus,
-      onlineStatus,
       isQueryManholeData: true
     })
-    const list = Array.isArray(res?.data) ? res.data : []
-    mockManholes.value = list.map((item, index) => normalizeDevice(item, index))
-    refreshLayerTree()
-    refreshBounds()
-    await nextTick()
-    renderMap()
+    
+    const result = res.data || {}
+    mockManholes.value = (result.devices || []).map((item, index) => normalizeDevice(item, index))
+    
+    // 同步统计信息
+    const stats = result.statistics || {}
+    console.log('[alarmSanalysis] 后端统计数据:', stats)
+    
+    // displayDevices computed 和 watch 自动触发树、地图刷新
   } catch (error) {
     mockManholes.value = []
-    ElMessage.error('井盖设备数据加载失败')
+    const msg = error?.message || error?.msg || '未知错误'
+    console.error('[alarmSanalysis] 加载失败:', error)
+    ElMessage.error('井盖设备数据加载失败: ' + msg)
   } finally {
     listLoading.value = false
   }
@@ -651,8 +708,14 @@ function resetView() {
 }
 
 const onLayerChange = (_data, checkedInfo) => {
-  visibleLayers.value = new Set(checkedInfo.checkedKeys)
-  loadDeviceList()
+  // check-strictly 模式下 checkedKeys 只含叶子节点,仅需排除 device-*
+  const statusKeys = checkedInfo.checkedKeys.filter(key => !key.startsWith('device-'))
+  const newSet = new Set(statusKeys)
+  const oldSet = visibleLayers.value
+  // 值未变时跳过,阻断 setCheckedKeys → onLayerChange → watch 循环
+  if (newSet.size === oldSet.size && [...newSet].every(k => oldSet.has(k))) return
+  visibleLayers.value = newSet
+  checkedKeys.value = statusKeys
 }
 const focusLayer = async (type) => {
   activeQuickFilter.value = activeQuickFilter.value === type ? '' : type
@@ -700,14 +763,16 @@ function handleSearch() {
 function handleReset() {
   queryParams.value.keyword = ''
   activeQuickFilter.value = ''
-  visibleLayers.value = new Set(defaultCheckedLayers)
-  layerTreeRef.value?.setCheckedKeys(defaultCheckedLayers)
+  visibleLayers.value = new Set(DEFAULT_CHECKED_KEYS)
+  checkedKeys.value = [...DEFAULT_CHECKED_KEYS]
   loadDeviceList()
 }
 
 watch(displayDevices, async () => {
   refreshBounds()
   await nextTick()
+  // 树数据变化后 el-tree 内部状态被重置,用 setCheckedKeys 恢复勾选
+  layerTreeRef.value?.setCheckedKeys(checkedKeys.value)
   renderMap()
 }, { deep: true })
 
@@ -715,10 +780,8 @@ onMounted(async () => {
   await nextTick()
   await initMap()
   await loadDeviceList()
-  nextTick(() => {
-    renderMap()
-    window.addEventListener('resize', renderMap)
-  })
+  // renderMap 由 watch(displayDevices) 自动触发,避免重复渲染
+  window.addEventListener('resize', renderMap)
 })
 onUnmounted(() => {
   window.removeEventListener('resize', renderMap)

+ 47 - 16
src/views/subSystem/manholeCover/alarm/index.vue

@@ -12,11 +12,12 @@
           clearable
           :prefix-icon="Search"
           size="small"
+          @input="handleSearch"
         />
 
         <div v-loading="listLoading" class="point-list">
           <div
-            v-for="point in filteredPoints"
+            v-for="point in deviceList"
             :key="point.equipmentId"
             class="point-item"
             :class="{ active: selectedPoint?.equipmentId === point.equipmentId }"
@@ -47,11 +48,23 @@
           </div>
 
           <el-empty
-            v-if="!listLoading && !filteredPoints.length"
+            v-if="!listLoading && !deviceList.length"
             description="暂无井盖监测设备"
             :image-size="120"
           />
         </div>
+
+        <div class="pagination-area">
+          <el-pagination
+            small
+            background
+            layout="total, prev, pager, next"
+            :total="total"
+            :page-size="pageSize"
+            v-model:current-page="currentPage"
+            @current-change="loadDeviceList"
+          />
+        </div>
       </div>
 
       <div v-if="selectedPoint" v-loading="detailLoading" class="config-panel">
@@ -178,12 +191,12 @@
 </template>
 
 <script setup>
-import {computed, onMounted, ref} from 'vue'
+import {onBeforeUnmount, onMounted, ref} from 'vue'
 import {ElMessage} from 'element-plus'
 import {Location, Monitor, Refresh, Search, Setting, Warning} from '@element-plus/icons-vue'
 import {
   getManholeAlarmDeviceDetail,
-  getManholeAlarmDeviceList,
+  getManholeDevicePage,
   saveManholeAlarmThreshold
 } from '@/api/pipeNetwork/basic'
 
@@ -226,10 +239,23 @@ const listLoading = ref(false)
 const detailLoading = ref(false)
 const saveLoading = ref(false)
 const deviceList = ref([])
+const total = ref(0)
+const currentPage = ref(1)
+const pageSize = 8
 const selectedPoint = ref(null)
 const selectedDetail = ref(null)
 const thresholds = ref(createEmptyThresholds())
 
+// 搜索防抖
+let searchTimer = null
+function handleSearch() {
+  clearTimeout(searchTimer)
+  searchTimer = setTimeout(() => {
+    currentPage.value = 1
+    loadDeviceList()
+  }, 300)
+}
+
 function createEmptyThresholds() {
   return thresholdConfigList.reduce((acc, item) => {
     acc[item.key] = {
@@ -259,16 +285,7 @@ function normalizeThresholdList(list) {
   return result
 }
 
-const filteredPoints = computed(() => {
-  const keyword = searchKeyword.value.trim().toLowerCase()
-  if (!keyword) return deviceList.value
-  return deviceList.value.filter(item => {
-    const name = `${item.equipmentName || ''}`.toLowerCase()
-    const code = `${item.equipmentCode || ''}`.toLowerCase()
-    const location = `${item.equipmentLocation || ''}`.toLowerCase()
-    return name.includes(keyword) || code.includes(keyword) || location.includes(keyword)
-  })
-})
+
 
 function getOnlineText(status) {
   return Number(status) === 1 ? '在线' : '离线'
@@ -324,11 +341,15 @@ function formatDateTime(value) {
 async function loadDeviceList() {
   listLoading.value = true
   try {
-    const res = await getManholeAlarmDeviceList({
+    const res = await getManholeDevicePage({
+      pageNum: currentPage.value,
+      pageSize,
+      deviceName: searchKeyword.value.trim() || null,
       isQueryManholeData: true
     })
-    const list = Array.isArray(res?.data) ? res.data : []
+    const list = res.data?.records || []
     deviceList.value = list
+    total.value = res.data?.total || list.length
 
     if (!list.length) {
       selectedPoint.value = null
@@ -432,6 +453,10 @@ async function saveThresholds() {
 onMounted(() => {
   loadDeviceList()
 })
+
+onBeforeUnmount(() => {
+  clearTimeout(searchTimer)
+})
 </script>
 
 <style scoped>
@@ -634,4 +659,10 @@ onMounted(() => {
   background: #fff;
   border-radius: 16px;
 }
+
+.pagination-area {
+  padding: 8px 0 0;
+  display: flex;
+  justify-content: center;
+}
 </style>

+ 225 - 285
src/views/subSystem/manholeCover/device/deviceApicture.vue

@@ -10,10 +10,10 @@
         <el-badge :value="alertCount" type="danger">
           <el-button type="danger" plain :icon="WarningFilled">告警设备</el-button>
         </el-badge>
-     </div>
+      </div>
     </div>
 
-    <!-- 主体区域:左侧设备列表 + 右侧GIS地图 -->
+    <!-- 主体区域:左侧设备列表 + 右侧百度地图 -->
     <div class="main-layout">
       <!-- 左侧:设备列表 & 筛选 -->
       <div class="device-panel">
@@ -25,79 +25,72 @@
               clearable
               :prefix-icon="Search"
               size="small"
+              @keyup.enter="handleSearch"
+              @clear="handleSearch"
           />
         </div>
         <div class="filter-tabs">
-          <el-radio-group v-model="deviceTypeFilter" size="small" @change="filterDevices">
+          <el-radio-group v-model="typeFilter" size="small" @change="handleSearch">
             <el-radio-button label="all">全部</el-radio-button>
-            <el-radio-button label="smart">智能井盖</el-radio-button>
-            <el-radio-button label="standard">普通井盖</el-radio-button>
-            <el-radio-button label="explosion">防爆井盖</el-radio-button>
+            <el-radio-button label="5">供水窨井</el-radio-button>
+            <el-radio-button label="9">排水窨井</el-radio-button>
+            <el-radio-button label="10">燃气窨井</el-radio-button>
           </el-radio-group>
         </div>
-        <div class="device-list-scroll">
+        <div class="device-list-scroll" v-loading="listLoading">
           <div
-              v-for="device in paginatedDevices"
+              v-for="device in devices"
               :key="device.id"
               class="device-item"
               :class="{ active: selectedDevice?.id === device.id }"
               @click="selectDevice(device)"
           >
             <div class="device-status-dot">
-              <el-badge :value="device.status === 'fault' ? '!' : ''" :type="device.status === 'fault' ? 'danger' : 'primary'">
-                <div class="status-icon" :style="{ backgroundColor: getStatusColor(device.status) }"></div>
-              </el-badge>
+              <div class="status-icon" :style="{ backgroundColor: getStatusColor(device.status) }"></div>
             </div>
             <div class="device-info">
               <div class="device-name">{{ device.name }}</div>
               <div class="device-location">
-                <el-icon><Location /></el-icon> {{ device.shortAddress }}
+                <el-icon><Location /></el-icon> {{ device.location || '未知位置' }}
               </div>
               <div class="device-meta">
-                <el-tag size="small" :type="getDeviceTypeTag(device.type)">{{ device.typeName }}</el-tag>
-                <span class="device-time">更新: {{ device.updateTime }}</span>
+                <el-tag size="small" :type="getStatusTag(device.status)">{{ getStatusText(device.status) }}</el-tag>
+                <span class="device-time">{{ device.typeName }}</span>
               </div>
             </div>
           </div>
+          <el-empty v-if="!listLoading && devices.length === 0" description="暂无设备数据" :image-size="80" />
           <!-- 分页 -->
           <div class="pagination-area">
             <el-pagination
                 small
                 background
                 layout="prev, pager, next"
-                :total="filteredDevices.length"
+                :total="total"
                 :page-size="pageSize"
                 v-model:current-page="currentPage"
+                @current-change="loadDevices"
             />
           </div>
         </div>
       </div>
 
-      <!-- 右侧:GIS地图模拟展示区 -->
+      <!-- 右侧:百度地图展示区 -->
       <div class="gis-container">
         <div class="map-toolbar">
           <div class="tool-group">
-            <el-button size="small" :icon="ZoomIn" @click="zoomIn">放大</el-button>
-            <el-button size="small" :icon="ZoomOut" @click="zoomOut">缩小</el-button>
-            <el-button size="small" :icon="RefreshRight" @click="resetView">重置视图</el-button>
+            <span class="map-title">沅陵县窨井设备分布</span>
           </div>
           <div class="legend">
             <span><i class="legend-dot online"></i> 在线</span>
             <span><i class="legend-dot offline"></i> 离线</span>
-            <span><i class="legend-dot fault"></i> 故障</span>
             <span><i class="legend-dot selected"></i> 选中设备</span>
           </div>
         </div>
 
-        <!-- 模拟的GIS地图区域 - 使用Canvas绘制并模拟标记点 -->
-        <div class="map-area" ref="mapContainer">
-          <canvas id="gisCanvas" ref="canvasRef" @click="handleMapClick"></canvas>
-          <!-- 悬浮信息卡 -->
-          <div v-if="hoverDevice" class="hover-tooltip" :style="{ top: hoverY + 'px', left: hoverX + 'px' }">
-            <div class="tooltip-title">{{ hoverDevice.name }}</div>
-            <div>状态: {{ getStatusText(hoverDevice.status) }}</div>
-            <div>位置: {{ hoverDevice.location }}</div>
-          </div>
+        <!-- 百度地图容器 -->
+        <div class="map-area">
+          <div id="deviceApictureMap" class="bmap-container"></div>
         </div>
 
         <!-- 右下角 选中设备详情卡片 -->
@@ -113,12 +106,12 @@
                 <el-descriptions-item label="设备名称">{{ selectedDevice.name }}</el-descriptions-item>
                 <el-descriptions-item label="设备类型">{{ selectedDevice.typeName }}</el-descriptions-item>
                 <el-descriptions-item label="运维状态">
-                  <el-tag :type="getStatusType(selectedDevice.status)" size="small">{{ getStatusText(selectedDevice.status) }}</el-tag>
+                  <el-tag :type="getStatusTag(selectedDevice.status)" size="small">{{ getStatusText(selectedDevice.status) }}</el-tag>
                 </el-descriptions-item>
-                <el-descriptions-item label="经纬度">{{ selectedDevice.lng }}, {{ selectedDevice.lat }}</el-descriptions-item>
+                <el-descriptions-item label="经纬度" v-if="selectedDevice.lng">{{ selectedDevice.lng }}, {{ selectedDevice.lat }}</el-descriptions-item>
                 <el-descriptions-item label="详细地址">{{ selectedDevice.location }}</el-descriptions-item>
                 <el-descriptions-item label="最后通讯">{{ selectedDevice.lastTime }}</el-descriptions-item>
-                <el-descriptions-item label="电池电量" v-if="selectedDevice.battery !== undefined">{{ selectedDevice.battery }}%</el-descriptions-item>
+                <el-descriptions-item label="电池电量" v-if="selectedDevice.battery !== null && selectedDevice.battery !== undefined">{{ selectedDevice.battery }}%</el-descriptions-item>
               </el-descriptions>
             </div>
           </div>
@@ -129,267 +122,225 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, reactive, nextTick } from 'vue'
-import { Location, Connection, WarningFilled, Search, ZoomIn, ZoomOut, RefreshRight, Close } from '@element-plus/icons-vue'
-
-// ---------- 模拟设备数据(含地理位置)----------
-const mockDevices = [
-  { id: 1, code: 'MN-1001', name: '人民路智能井盖', type: 'smart', typeName: '智能井盖', status: 'online', lng: 121.487, lat: 31.249, location: '人民路与解放路口东50m', shortAddress: '人民路解放路口', updateTime: '10:23', lastTime: '2025-04-02 10:23:11', battery: 87 },
-  { id: 2, code: 'MN-1002', name: '滨江路防爆井盖', type: 'explosion', typeName: '防爆井盖', status: 'maintenance', lng: 121.502, lat: 31.235, location: '滨江路化工园区南门', shortAddress: '滨江路化工园', updateTime: '09:45', lastTime: '2025-04-02 09:45:33', battery: 34 },
-  { id: 3, code: 'MN-1003', name: '老城区铸铁井盖', type: 'standard', typeName: '普通井盖', status: 'fault', lng: 121.478, lat: 31.258, location: '古城街北段树下', shortAddress: '古城街北段', updateTime: '昨日', lastTime: '2025-04-01 16:20:10', battery: null },
-  { id: 4, code: 'MN-1004', name: '高新区智能井盖', type: 'smart', typeName: '智能井盖', status: 'online', lng: 121.512, lat: 31.242, location: '高新大道云计算中心旁', shortAddress: '高新大道云中心', updateTime: '11:02', lastTime: '2025-04-02 11:02:45', battery: 96 },
-  { id: 5, code: 'MN-1005', name: '南港路井盖', type: 'standard', typeName: '普通井盖', status: 'offline', lng: 121.495, lat: 31.228, location: '南港路旧车站段', shortAddress: '南港路车站', updateTime: '2025-03-30', lastTime: '2025-03-30 14:00:00', battery: null },
-  { id: 6, code: 'SM-2001', name: '智慧园林监测井盖', type: 'smart', typeName: '智能井盖', status: 'online', lng: 121.466, lat: 31.265, location: '中央公园北门', shortAddress: '中央公园', updateTime: '10:55', lastTime: '2025-04-02 10:55:00', battery: 72 },
-  { id: 7, code: 'EX-3002', name: '石化区防爆井盖', type: 'explosion', typeName: '防爆井盖', status: 'maintenance', lng: 121.524, lat: 31.239, location: '石化大道与滨海路交叉口', shortAddress: '石化大道', updateTime: '08:30', lastTime: '2025-04-02 08:30:22', battery: 41 }
-]
+import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
+import { Location, Connection, WarningFilled, Search, Close } from '@element-plus/icons-vue'
+import { getManholeDevicePage, getManholeAlarmDeviceList } from '@/api/pipeNetwork/basic'
+import locationIcon from '@/assets/images/location.png'
+import { ElMessage } from 'element-plus'
 
-// 状态筛选及分页
+// ==================== 分页查询(接口数据) ====================
 const searchKeyword = ref('')
-const deviceTypeFilter = ref('all')
+const typeFilter = ref('all')
 const currentPage = ref(1)
-const pageSize = 6
+const pageSize = 8
+const devices = ref([])
+const total = ref(0)
+const listLoading = ref(false)
 
-const filteredDevices = computed(() => {
-  let list = mockDevices
-  if (searchKeyword.value) {
-    const kw = searchKeyword.value.toLowerCase()
-    list = list.filter(d => d.name.toLowerCase().includes(kw) || d.code.toLowerCase().includes(kw))
-  }
-  if (deviceTypeFilter.value !== 'all') {
-    list = list.filter(d => d.type === deviceTypeFilter.value)
-  }
-  return list
-})
+// 顶部统计:在线设备数 / 告警设备数
+const onlineCount = ref(0)
+const alertCount = ref(0)
 
-const paginatedDevices = computed(() => {
-  const start = (currentPage.value - 1) * pageSize
-  return filteredDevices.value.slice(start, start + pageSize)
-})
+// 加载分页设备列表(同时重建地图标记)
+async function loadDevices() {
+  listLoading.value = true
+  try {
+    const res = await getManholeDevicePage({
+      pageNum: currentPage.value,
+      pageSize,
+      deviceName: searchKeyword.value.trim() || null,
+      equipmentTypeId: typeFilter.value === 'all' ? null : typeFilter.value,
+      isQueryManholeData: true
+    })
+    const rows = res.data?.records || []
+    devices.value = rows.map(d => ({
+      id: d.equipmentId,
+      code: d.equipmentCode,
+      name: d.equipmentName,
+      equipmentTypeId: d.equipmentTypeId,
+      typeName: d.equipmentTypeName,
+      location: d.equipmentLocation,
+      status: Number(d.equipmentStatus?.onlineStatus) === 1 ? 'online' : 'offline',
+      lng: d.longitude != null ? Number(d.longitude) : null,
+      lat: d.latitude != null ? Number(d.latitude) : null,
+      battery: d.manholeData?.batteryLevel ?? null,
+      lastTime: d.manholeData?.uploadTime || d.manholeData?.createTime || '-'
+    }))
+    total.value = res.data?.total || rows.length
+    refreshMarkers()
+  } catch (e) {
+    ElMessage.error('设备列表加载失败')
+  } finally {
+    listLoading.value = false
+  }
+}
 
-// 统计在线/告警数量
-const onlineCount = computed(() => mockDevices.filter(d => d.status === 'online').length)
-const alertCount = computed(() => mockDevices.filter(d => d.status === 'fault' || d.status === 'maintenance').length)
+// 顶部统计:全量在线数 + 待处理告警设备数
+async function loadOverview() {
+  try {
+    const [onlineRes, alarmRes] = await Promise.all([
+      getManholeDevicePage({ pageNum: 1, pageSize: 1000, isQueryManholeData: false }),
+      getManholeAlarmDeviceList({ alarmStatus: 1, isQueryManholeData: true })
+    ])
+    const rows = onlineRes.data?.records || []
+    onlineCount.value = rows.filter(d => Number(d.equipmentStatus?.onlineStatus) === 1).length
+    alertCount.value = alarmRes.data?.records?.length || 0
+  } catch (e) {
+    onlineCount.value = 0
+    alertCount.value = 0
+  }
+}
 
-// GIS地图画布相关
-const mapContainer = ref(null)
-const canvasRef = ref(null)
-let ctx = null
-let mapWidth = 0, mapHeight = 0
-let mapZoom = 1
-let mapOffsetX = 0, mapOffsetY = 0  // 用于拖拽偏移(简易)
-const baseBounds = { minX: 121.45, maxX: 121.55, minY: 31.22, maxY: 31.28 } // 经纬度范围
+const handleSearch = () => {
+  currentPage.value = 1
+  loadDevices()
+}
 
-// 选中的设备
+// ==================== 选中设备 ====================
 const selectedDevice = ref(null)
-const hoverDevice = ref(null)
-const hoverX = ref(0), hoverY = ref(0)
 
-function getDeviceScreenPos(lng, lat) {
-  // 将经度/纬度映射到canvas坐标 (模拟墨卡托简易映射)
-  const xPercent = (lng - baseBounds.minX) / (baseBounds.maxX - baseBounds.minX)
-  const yPercent = 1 - (lat - baseBounds.minY) / (baseBounds.maxY - baseBounds.minY)
-  let x = xPercent * mapWidth
-  let y = yPercent * mapHeight
-  // 应用缩放和平移 (目前平移没有实现拖动,留出接口)
-  const centerX = mapWidth / 2, centerY = mapHeight / 2
-  x = (x - centerX) * mapZoom + centerX + mapOffsetX
-  y = (y - centerY) * mapZoom + centerY + mapOffsetY
-  return { x, y }
-}
-
-function drawMapBackground() {
-  if (!ctx) return
-  // 绘制模拟地图样式:网格+绿地/道路风格
-  ctx.fillStyle = "#e9f5e8"
-  ctx.fillRect(0, 0, mapWidth, mapHeight)
-  // 绘制道路网格线(示意)
-  ctx.beginPath()
-  ctx.strokeStyle = "#dcdcdc"
-  ctx.lineWidth = 1
-  for (let i = 0; i < 10; i++) {
-    const x = (i / 10) * mapWidth
-    ctx.moveTo(x, 0)
-    ctx.lineTo(x, mapHeight)
-    ctx.stroke()
-    const y = (i / 10) * mapHeight
-    ctx.moveTo(0, y)
-    ctx.lineTo(mapWidth, y)
-    ctx.stroke()
+function selectDevice(device) {
+  selectedDevice.value = device
+  // 地图中心定位到该设备(无坐标则不移动)
+  if (mapInstance && device.lng && device.lat) {
+    mapInstance.centerAndZoom(new BMapGL.Point(device.lng, device.lat), 16)
   }
-  // 绘制一些绿地色块
-  ctx.fillStyle = "#b8e0b4"
-  ctx.fillRect(20, 30, 150, 100)
-  ctx.fillStyle = "#a0cf9c"
-  ctx.fillRect(mapWidth-180, 60, 160, 120)
-  ctx.fillStyle = "#c8e6c9"
-  ctx.fillRect(80, mapHeight-140, 200, 80)
-  // 主要河流示意
-  ctx.beginPath()
-  ctx.strokeStyle = "#4fc3f7"
-  ctx.lineWidth = 8
-  ctx.moveTo(120, mapHeight-60)
-  ctx.quadraticCurveTo(mapWidth/2, mapHeight-150, mapWidth-80, mapHeight-30)
-  ctx.stroke()
+  highlightMarker(device.id)
 }
 
-function drawAllDevices() {
-  if (!ctx) return
-  mockDevices.forEach(device => {
-    const { x, y } = getDeviceScreenPos(device.lng, device.lat)
-    // 超出画布范围但允许简单绘制
-    if (x < -20 || x > mapWidth+20 || y < -20 || y > mapHeight+20) return
-    // 根据状态选择图标样式
-    let color = "#67C23A"  // online
-    if (device.status === 'offline') color = "#909399"
-    if (device.status === 'fault') color = "#F56C6C"
-    if (device.status === 'maintenance') color = "#E6A23C"
-    if (selectedDevice.value?.id === device.id) {
-      ctx.shadowBlur = 12
-      ctx.shadowColor = "rgba(64,158,255,0.8)"
-      // 外发光
-      ctx.beginPath()
-      ctx.arc(x, y, 16, 0, 2 * Math.PI)
-      ctx.fillStyle = "rgba(64,158,255,0.2)"
-      ctx.fill()
-    } else {
-      ctx.shadowBlur = 0
-    }
-    // 绘制标记:圆形+井盖样式
-    ctx.beginPath()
-    ctx.arc(x, y, 12, 0, 2 * Math.PI)
-    ctx.fillStyle = color
-    ctx.fill()
-    ctx.strokeStyle = "#fff"
-    ctx.lineWidth = 2
-    ctx.stroke()
-    ctx.fillStyle = "#ffffff"
-    ctx.font = "bold 14px 'Segoe UI'"
-    ctx.shadowBlur = 0
-    ctx.fillText(device.name.length > 6 ? device.name.slice(0,4)+'..' : device.name, x-18, y-8)
-    // 若故障显示感叹号
-    if (device.status === 'fault') {
-      ctx.fillStyle = "#fff"
-      ctx.font = "bold 14px sans-serif"
-      ctx.fillText("!", x-4, y+5)
-    }
-  })
+function clearSelected() {
+  selectedDevice.value = null
+  clearMarkerHighlight()
 }
 
-function renderMap() {
-  if (!canvasRef.value) return
-  const canvas = canvasRef.value
-  mapWidth = canvas.parentElement.clientWidth
-  mapHeight = canvas.parentElement.clientHeight
-  canvas.width = mapWidth
-  canvas.height = mapHeight
-  ctx = canvas.getContext('2d')
-  drawMapBackground()
-  drawAllDevices()
+// ==================== 百度地图(BMapGL,中心定位沅陵县) ====================
+let mapInstance = null
+const markerMap = new Map()
+
+// 沅陵县中心坐标
+const YUANLING_CENTER = { lng: 110.393, lat: 28.452 }
+
+function initMap() {
+  const container = document.getElementById('deviceApictureMap')
+  if (!container || typeof BMapGL === 'undefined') {
+    console.warn('BMapGL 未加载或容器不存在')
+    return
+  }
+  try {
+    mapInstance = new BMapGL.Map('deviceApictureMap')
+    mapInstance.centerAndZoom(new BMapGL.Point(YUANLING_CENTER.lng, YUANLING_CENTER.lat), 13)
+    mapInstance.enableScrollWheelZoom(true)
+  } catch (e) {
+    console.error('百度地图初始化失败:', e)
+  }
 }
 
-function zoomIn() {
-  mapZoom = Math.min(mapZoom + 0.1, 2.5)
-  renderMap()
+function getStatusColor(status) {
+  if (status === 'online') return '#67C23A'
+  if (status === 'offline') return '#909399'
+  return '#E6A23C'
 }
-function zoomOut() {
-  mapZoom = Math.max(mapZoom - 0.1, 0.6)
-  renderMap()
+
+// 添加设备地图标记(状态色名称气泡 + 定位图标)
+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 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}
+    </div>
+    <div><img src="${locationIcon}" style="width:28px;height:28px;display:block;margin:0 auto;"/></div>
+  </div>`
+
+  const label = new BMapGL.Label(html, {
+    position: bPoint,
+    offset: new BMapGL.Size(-30, -50)
+  })
+  label.setStyle({
+    border: 'none',
+    background: 'transparent',
+    padding: '0',
+    zIndex: '10'
+  })
+  label.addEventListener('click', function () {
+    selectDevice(device)
+  })
+  mapInstance.addOverlay(label)
+  markerMap.set(device.id, label)
 }
-function resetView() {
-  mapZoom = 1
-  mapOffsetX = 0
-  mapOffsetY = 0
-  renderMap()
+
+// 重建地图标记(跟随分页/筛选/搜索结果)
+function refreshMarkers() {
+  if (!mapInstance) return
+  markerMap.forEach(label => mapInstance.removeOverlay(label))
+  markerMap.clear()
+  devices.value.forEach(device => addMapMarker(device))
 }
 
-// 处理地图点击,根据位置选择设备
-function handleMapClick(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  let minDist = 20
-  let clickedDevice = null
-  mockDevices.forEach(device => {
-    const { x, y } = getDeviceScreenPos(device.lng, device.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) {
-      minDist = dist
-      clickedDevice = device
+// 高亮选中标记(放大地图标记气泡)
+function highlightMarker(id) {
+  markerMap.forEach((label, key) => {
+    const dom = label.getContentContainer ? label.getContentContainer() : null
+    if (dom) {
+      const isSelected = key === id
+      dom.style.zIndex = isSelected ? '999' : '10'
+      const title = dom.querySelector('div')
+      if (title) {
+        title.style.transform = isSelected ? 'scale(1.15)' : 'scale(1)'
+        title.style.transition = 'transform 0.2s'
+        title.style.boxShadow = isSelected ? '0 0 0 2px #409eff' : '0 1px 4px rgba(0,0,0,0.3)'
+      }
     }
   })
-  if (clickedDevice) {
-    selectedDevice.value = clickedDevice
-  } else {
-    selectedDevice.value = null
-  }
 }
 
-// 鼠标悬浮效果(在canvas上跟踪设备悬浮)
-function onCanvasMouseMove(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  let hover = null
-  let minDist = 15
-  mockDevices.forEach(device => {
-    const { x, y } = getDeviceScreenPos(device.lng, device.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) {
-      minDist = dist
-      hover = device
-      hoverX.value = e.clientX + 10
-      hoverY.value = e.clientY - 30
+function clearMarkerHighlight() {
+  markerMap.forEach((label, key) => {
+    const dom = label.getContentContainer ? label.getContentContainer() : null
+    if (dom) {
+      dom.style.zIndex = '10'
+      const title = dom.querySelector('div')
+      if (title) {
+        title.style.transform = 'scale(1)'
+        title.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)'
+      }
     }
   })
-  hoverDevice.value = hover
 }
 
-function selectDevice(device) {
-  selectedDevice.value = device
-  // 可选: 地图中心移动到设备(如果需要可触发pan,但保持简洁)
-  // 可以提示位置闪烁(重新绘制)
-  renderMap()
-}
-function clearSelected() {
-  selectedDevice.value = null
-  renderMap()
-}
-function filterDevices() {
-  currentPage.value = 1
-  // 仅影响左侧列表,地图上仍旧展示全部(可视化管理展示全局)
-  renderMap()
+// 地图自适应窗口变化
+const handleResize = () => {
+  mapInstance && mapInstance.resize && mapInstance.resize()
 }
 
-function getStatusColor(status) {
-  if (status === 'online') return '#67C23A'
-  if (status === 'offline') return '#909399'
-  if (status === 'fault') return '#F56C6C'
-  return '#E6A23C'
-}
-function getStatusType(status) {
-  if (status === 'online') return 'success'
-  if (status === 'offline') return 'info'
-  if (status === 'fault') return 'danger'
-  return 'warning'
-}
+// ==================== 状态文案 ====================
 function getStatusText(status) {
-  const map = { online: '在线', offline: '离线', fault: '故障', maintenance: '需维护' }
+  const map = { online: '在线', offline: '离线' }
   return map[status] || status
 }
-function getDeviceTypeTag(type) {
-  if (type === 'smart') return 'success'
-  if (type === 'explosion') return 'danger'
-  return ''
+function getStatusTag(status) {
+  const map = { online: 'success', offline: 'info' }
+  return map[status] || ''
 }
 
-onMounted(() => {
-  nextTick(() => {
-    renderMap()
-    window.addEventListener('resize', () => renderMap())
-    if (canvasRef.value) {
-      canvasRef.value.addEventListener('mousemove', onCanvasMouseMove)
-    }
-  })
+// ==================== 生命周期 ====================
+onMounted(async () => {
+  await nextTick()
+  initMap()
+  window.addEventListener('resize', handleResize)
+  loadOverview()
+  loadDevices()
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', handleResize)
+  if (mapInstance) {
+    mapInstance.clearOverlays && mapInstance.clearOverlays()
+    mapInstance = null
+  }
+  markerMap.clear()
 })
 </script>
 
@@ -407,23 +358,13 @@ onMounted(() => {
 }
 .dashboard-header {
   display: flex;
-  justify-content: space-between;
+  justify-content: flex-end;
   align-items: center;
   background: white;
   padding: 8px 24px;
   box-shadow: 0 2px 8px rgba(0,0,0,0.05);
   z-index: 10;
 }
-.logo-area {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-.logo-area .title {
-  font-size: 20px;
-  font-weight: 600;
-  color: #1f2d3d;
-}
 .header-actions {
   display: flex;
   gap: 16px;
@@ -453,6 +394,7 @@ onMounted(() => {
   display: flex;
   justify-content: space-between;
   align-items: center;
+  gap: 10px;
 }
 .filter-tabs {
   padding: 10px 12px;
@@ -488,10 +430,14 @@ onMounted(() => {
 }
 .device-info {
   flex: 1;
+  min-width: 0;
 }
 .device-name {
   font-weight: 600;
   margin-bottom: 4px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 .device-location {
   font-size: 12px;
@@ -499,6 +445,9 @@ onMounted(() => {
   display: flex;
   align-items: center;
   gap: 4px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 .device-meta {
   display: flex;
@@ -526,6 +475,11 @@ onMounted(() => {
   background: white;
   z-index: 2;
 }
+.map-title {
+  font-weight: 600;
+  color: #303133;
+  font-size: 14px;
+}
 .legend {
   display: flex;
   gap: 16px;
@@ -541,29 +495,15 @@ onMounted(() => {
 }
 .legend-dot.online { background-color: #67C23A; }
 .legend-dot.offline { background-color: #909399; }
-.legend-dot.fault { background-color: #F56C6C; }
 .legend-dot.selected { background-color: #409eff; box-shadow: 0 0 0 2px rgba(64,158,255,0.4); }
 .map-area {
   flex: 1;
   position: relative;
-  background: #eef2ea;
-  cursor: crosshair;
+  min-height: 0;
 }
-canvas {
+.bmap-container {
   width: 100%;
   height: 100%;
-  display: block;
-}
-.hover-tooltip {
-  position: fixed;
-  background: rgba(0,0,0,0.75);
-  color: white;
-  padding: 6px 12px;
-  border-radius: 8px;
-  font-size: 12px;
-  pointer-events: none;
-  z-index: 100;
-  white-space: nowrap;
 }
 .detail-card {
   position: absolute;
@@ -607,4 +547,4 @@ canvas {
   display: flex;
   justify-content: center;
 }
-</style>
+</style>

+ 401 - 312
src/views/subSystem/manholeCover/device/index.vue

@@ -1,16 +1,15 @@
-<!--监测设备台账-->
+<!--监测设备台账(窨井设备)-->
 <template>
   <div class="manhole-cover-management">
     <!-- 页面头部 -->
     <div class="page-header">
-
       <div class="stats-section">
         <el-card class="stat-card" shadow="hover">
           <div class="stat-content">
             <el-icon><Monitor /></el-icon>
             <div>
-              <div class="stat-label">在设备</div>
-              <div class="stat-number">{{ activeDevicesCount }}</div>
+              <div class="stat-label">在线设备</div>
+              <div class="stat-number">{{ onlineDevicesCount }}</div>
             </div>
           </div>
         </el-card>
@@ -18,8 +17,8 @@
           <div class="stat-content">
             <el-icon><CircleClose /></el-icon>
             <div>
-              <div class="stat-label">已用/停用</div>
-              <div class="stat-number">{{ inactiveDevicesCount }}</div>
+              <div class="stat-label">离线设备</div>
+              <div class="stat-number">{{ offlineDevicesCount }}</div>
             </div>
           </div>
         </el-card>
@@ -27,8 +26,8 @@
           <div class="stat-content">
             <el-icon><Location /></el-icon>
             <div>
-              <div class="stat-label">监测点<br/>覆盖区域</div>
-              <div class="stat-number">{{ locationsCount }}</div>
+              <div class="stat-label">设备总数</div>
+              <div class="stat-number">{{ totalDevicesCount }}</div>
             </div>
           </div>
         </el-card>
@@ -38,364 +37,340 @@
     <!-- 筛选工具栏 -->
     <div class="filter-bar">
       <el-input
-          v-model="searchKeyword"
-          placeholder="搜索设备编号/名称/位置"
-          clearable
-          style="width: 240px"
-          :prefix-icon="Search"
+        v-model="searchKeyword"
+        placeholder="搜索设备名称"
+        clearable
+        style="width: 240px"
+        :prefix-icon="Search"
+        @keyup.enter="handleSearch"
+        @clear="handleSearch"
       />
-      <el-select v-model="statusFilter" placeholder="运维状态" clearable style="width: 140px">
-        <el-option label="全部" value="" />
-        <el-option label="正常运行" value="normal" />
-        <el-option label="需维护" value="maintenance" />
-        <el-option label="故障" value="fault" />
-        <el-option label="停用" value="inactive" />
-      </el-select>
-      <el-select v-model="typeFilter" placeholder="设备类型" clearable style="width: 140px">
-        <el-option label="全部" value="" />
-        <el-option label="普通井盖" value="standard" />
-        <el-option label="智能井盖" value="smart" />
-        <el-option label="防爆井盖" value="explosion" />
+      <el-select v-model="typeFilter" placeholder="设备类型" clearable style="width: 160px" @change="handleSearch">
+        <el-option v-for="t in typeOptions" :key="t.value" :label="t.label" :value="t.value" />
       </el-select>
       <el-button type="primary" :icon="Refresh" @click="resetFilters">重置筛选</el-button>
     </div>
 
-    <!-- 设备列表展示区 - 卡片 + 表格融合风格 -->
+    <!-- 设备列表展示区 -->
     <div class="device-list-container">
       <el-table
-          :data="filteredDevices"
-          stripe
-          border
-          style="width: 100%"
-          :row-class-name="tableRowClassName"
-          @row-click="handleRowClick"
+        :data="devices"
+        v-loading="loading"
+        stripe
+        border
+        style="width: 100%"
+        :row-class-name="tableRowClassName"
+        @row-click="handleRowClick"
       >
-        <el-table-column type="expand">
-          <template #default="{ row }">
-            <div class="expand-detail">
-              <el-descriptions title="设备历史安装信息" :column="2" border size="small">
-                <el-descriptions-item label="首次安装时间">{{ row.installHistory?.firstInstall || '—' }}</el-descriptions-item>
-                <el-descriptions-item label="最近安装时间">{{ row.installHistory?.lastInstall || '—' }}</el-descriptions-item>
-                <el-descriptions-item label="历史安装次数">{{ row.installHistory?.installCount || 0 }}次</el-descriptions-item>
-                <el-descriptions-item label="历史测点记录">{{ row.installHistory?.previousLocations?.join(', ') || '无' }}</el-descriptions-item>
-                <el-descriptions-item label="备注">{{ row.installHistory?.remark || '暂无' }}</el-descriptions-item>
-              </el-descriptions>
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column prop="deviceId" label="设备编号" width="140" sortable />
-        <el-table-column prop="deviceName" label="设备名称" width="160" show-overflow-tooltip />
-        <el-table-column prop="deviceType" label="设备类型" width="110">
+        <el-table-column prop="deviceCode" label="设备编号" width="150" show-overflow-tooltip />
+        <el-table-column prop="deviceName" label="设备名称" width="180" show-overflow-tooltip />
+        <el-table-column prop="typeName" label="设备类型" width="130">
           <template #default="{ row }">
-            <el-tag :type="getDeviceTypeTag(row.deviceType)">{{ getDeviceTypeText(row.deviceType) }}</el-tag>
+            <el-tag size="small" effect="plain">{{ getTypeText(row) }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="location" label="所在测点及位置" min-width="180" show-overflow-tooltip>
-          <template #default="{ row }">
-            <div><el-icon><Location /></el-icon> {{ row.location }}</div>
-            <div style="font-size:12px; color:#909399;">测点ID: {{ row.measurePointId }}</div>
-          </template>
-        </el-table-column>
-        <el-table-column prop="status" label="运维状态" width="120" align="center">
-          <template #default="{ row }">
-            <el-badge :value="row.status === 'fault' ? '告警' : ''" :type="row.status === 'fault' ? 'danger' : 'primary'">
-              <el-tag :type="getStatusType(row.status)" effect="plain">{{ getStatusText(row.status) }}</el-tag>
-            </el-badge>
-          </template>
+        <el-table-column prop="location" label="设备位置" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="lastMaintainDate" label="最近运维时间" width="130">
+          <template #default="{ row }">{{ row.lastMaintainDate || '—' }}</template>
         </el-table-column>
-        <el-table-column prop="lastMaintenance" label="最近运维时间" width="160" sortable />
-        <el-table-column prop="operationStatus" label="在用/已用" width="110" align="center">
+        <el-table-column prop="onlineStatus" label="在线状态" width="110" align="center">
           <template #default="{ row }">
-            <el-switch
-                v-model="row.isActive"
-                disabled
-                active-text="在用"
-                inactive-text="已用"
-                active-color="#13ce66"
-                inactive-color="#909399"
-            />
+            <el-tag :type="getOnlineTagType(row.onlineStatus)" size="small">{{ getOnlineText(row.onlineStatus) }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="100" fixed="right">
+        <el-table-column label="操作" width="180" fixed="right">
           <template #default="{ row }">
             <el-button link type="primary" size="small" @click.stop="showDetail(row)">详情</el-button>
+            <el-button link type="success" size="small" @click.stop="openRelDialog(row)">关联井下设备</el-button>
           </template>
         </el-table-column>
       </el-table>
 
-      <!-- 简易分页 -->
+      <!-- 分页 -->
       <div class="pagination-wrapper">
         <el-pagination
-            background
-            layout="prev, pager, next"
-            :total="filteredDevices.length"
-            :page-size="pageSize"
-            v-model:current-page="currentPage"
+          background
+          layout="total, prev, pager, next"
+          :total="total"
+          :page-size="pageSize"
+          v-model:current-page="currentPage"
+          @current-change="loadDevices"
         />
       </div>
     </div>
 
-    <!-- 右侧侧边详情卡片 (动态监测信息面板) -->
-    <el-drawer v-model="drawerVisible" title="设备电子档案 - 详细信息" size="40%" direction="rtl">
+    <!-- 设备详情抽屉 -->
+    <el-drawer v-model="drawerVisible" title="设备电子档案 - 详细信息" size="40%" direction="rtl" v-loading="detailLoading">
       <template v-if="selectedDevice">
         <el-descriptions :column="1" border>
-          <el-descriptions-item label="设备编号">{{ selectedDevice.deviceId }}</el-descriptions-item>
+          <el-descriptions-item label="设备编号">{{ selectedDevice.deviceCode }}</el-descriptions-item>
           <el-descriptions-item label="设备名称">{{ selectedDevice.deviceName }}</el-descriptions-item>
-          <el-descriptions-item label="设备类型">{{ getDeviceTypeText(selectedDevice.deviceType) }}</el-descriptions-item>
-          <el-descriptions-item label="运维状态">
-            <el-tag :type="getStatusType(selectedDevice.status)">{{ getStatusText(selectedDevice.status) }}</el-tag>
+          <el-descriptions-item label="设备类型">{{ getTypeText(selectedDevice) }}</el-descriptions-item>
+          <el-descriptions-item label="在线状态">
+            <el-tag :type="getOnlineTagType(selectedDevice.onlineStatus)">{{ getOnlineText(selectedDevice.onlineStatus) }}</el-tag>
           </el-descriptions-item>
-          <el-descriptions-item label="在用状态">
-            <el-tag :type="selectedDevice.isActive ? 'success' : 'info'">{{ selectedDevice.isActive ? '在用' : '已用' }}</el-tag>
-          </el-descriptions-item>
-          <el-descriptions-item label="所在测点ID">{{ selectedDevice.measurePointId }}</el-descriptions-item>
-          <el-descriptions-item label="详细地理位置">{{ selectedDevice.location }}</el-descriptions-item>
-          <el-descriptions-item label="最近运维时间">{{ selectedDevice.lastMaintenance }}</el-descriptions-item>
-          <el-descriptions-item label="电池电量/健康度" v-if="selectedDevice.battery">{{ selectedDevice.battery }}%</el-descriptions-item>
+          <el-descriptions-item label="设备位置">{{ selectedDevice.location || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="最近运维时间">{{ selectedDevice.lastMaintainDate || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="电池电量/健康度" v-if="selectedDevice.batteryLevel !== null && selectedDevice.batteryLevel !== undefined && selectedDevice.batteryLevel !== ''">{{ selectedDevice.batteryLevel }}%</el-descriptions-item>
           <el-descriptions-item label="最后通讯时间">{{ selectedDevice.lastCommunication || '—' }}</el-descriptions-item>
         </el-descriptions>
 
-        <el-divider content-position="left">历史安装记录</el-divider>
-        <el-timeline>
-          <el-timeline-item
-              v-for="(record, idx) in selectedDevice.historyRecords"
-              :key="idx"
-              :timestamp="record.time"
-              placement="top"
-              :type="record.type"
-          >
-            {{ record.content }}
-          </el-timeline-item>
-          <el-timeline-item v-if="!selectedDevice.historyRecords?.length" timestamp="暂无" placement="top">
-            尚无历史安装迁移记录
-          </el-timeline-item>
-        </el-timeline>
         <el-divider content-position="left">动态监测指标</el-divider>
         <div class="monitor-metrics">
           <el-row :gutter="12">
-            <el-col :span="12"><el-statistic title="井盖倾斜角" :value="selectedDevice.metrics?.tilt || 0" suffix="°" /></el-col>
+            <el-col :span="12"><el-statistic title="井盖倾斜角" :value="selectedDevice.metrics?.tilt ?? '—'" suffix="°" /></el-col>
             <el-col :span="12"><el-statistic title="水浸状态" :value="selectedDevice.metrics?.waterIntrusion ? '已浸水' : '正常'" /></el-col>
-            <el-col :span="12"><el-statistic title="气体浓度(ppm)" :value="selectedDevice.metrics?.gas || 0" /></el-col>
-            <el-col :span="12"><el-statistic title="振动强度" :value="selectedDevice.metrics?.vibration || 0" suffix="Hz" /></el-col>
+            <el-col :span="12"><el-statistic title="温度" :value="selectedDevice.metrics?.temperature ?? '—'" suffix="℃" /></el-col>
+            <el-col :span="12"><el-statistic title="信号量" :value="selectedDevice.metrics?.signal ?? '—'" /></el-col>
           </el-row>
         </div>
+
+        <el-divider content-position="left">预警阈值</el-divider>
+        <el-table v-if="selectedDevice.warningThresholds.length" :data="selectedDevice.warningThresholds" size="small" border>
+          <el-table-column prop="warningType" label="预警类型" width="100" />
+          <el-table-column prop="warningCode" label="预警编码" width="120" />
+          <el-table-column prop="minValue" label="最小值" width="90" />
+          <el-table-column prop="maxValue" label="最大值" width="90" />
+          <el-table-column prop="remark" label="备注" show-overflow-tooltip />
+        </el-table>
+        <div v-else style="color:#909399;font-size:13px;">暂无预警阈值配置</div>
+
         <div style="margin-top: 20px; text-align: right;">
           <el-button type="primary" @click="closeDrawer">关闭</el-button>
         </div>
       </template>
     </el-drawer>
+
+    <!-- 关联设备管理对话框 -->
+    <el-dialog v-model="relDialogVisible" :title="`井下设备关联 - ${relDevice?.deviceName || ''}`" width="860px" @close="onRelDialogClose">
+      <div class="rel-container">
+        <!-- 已关联设备列表 -->
+        <div class="rel-section">
+          <div class="rel-section-title">
+            <span>已关联设备</span>
+            <el-tag type="info" size="small">{{ relList.length }} 台</el-tag>
+          </div>
+          <el-table :data="relList" border size="small" v-loading="relLoading" max-height="240">
+            <el-table-column prop="equipmentCode" label="设备编号" min-width="140" show-overflow-tooltip />
+            <el-table-column prop="equipmentName" label="设备名称" min-width="140" show-overflow-tooltip />
+            <el-table-column prop="equipmentLocation" label="位置" min-width="160" show-overflow-tooltip />
+            <el-table-column label="操作" width="90" align="center">
+              <template #default="{ row }">
+                <el-button link type="danger" size="small" @click="removeRel(row)">取消关联</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+
+        <!-- 搜索可选设备 -->
+        <div class="rel-section">
+          <div class="rel-section-title">
+            <span>添加关联设备</span>
+            <el-input
+              v-model="relSearchKeyword"
+              placeholder="搜索设备编号/名称/位置"
+              clearable
+              size="small"
+              style="width: 260px"
+              :prefix-icon="Search"
+              @keyup.enter="searchRelDevices"
+              @clear="searchRelDevices"
+            />
+          </div>
+          <el-table :data="searchResults" border size="small" v-loading="relSearching" max-height="240" @selection-change="handleSearchSelectionChange">
+            <el-table-column type="selection" width="45" />
+            <el-table-column prop="equipmentCode" label="设备编号" min-width="140" show-overflow-tooltip />
+            <el-table-column prop="equipmentName" label="设备名称" min-width="140" show-overflow-tooltip />
+            <el-table-column prop="equipmentLocation" label="位置" min-width="160" show-overflow-tooltip />
+            <el-table-column label="类型" width="120">
+              <template #default="{ row }">
+                <el-tag size="small" effect="plain">{{ getTypeText(row) }}</el-tag>
+              </template>
+            </el-table-column>
+          </el-table>
+          <div class="rel-search-footer">
+            <el-button type="primary" size="small" :disabled="!searchSelection.length" :loading="relSaving" @click="batchAddRel">关联选中设备 ({{ searchSelection.length }})</el-button>
+          </div>
+        </div>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
+import { ref, onMounted } from 'vue'
 import { Monitor, CircleClose, Location, Search, Refresh } from '@element-plus/icons-vue'
+import { getManholeDevicePage, getManholeDeviceById, getEquipmentTypeList, searchManholeRelDevices, getManholeDeviceRels, addManholeDeviceRel, deleteManholeDeviceRel } from '@/api/pipeNetwork/basic'
+import { ElMessage } from 'element-plus'
 
-// --- 模拟设备数据 ---
-const mockDevices = [
-  {
-    deviceId: 'MN-1001',
-    deviceName: '人民路智能井盖',
-    deviceType: 'smart',
-    location: '人民路与解放路口东50m',
-    measurePointId: 'MP-A021',
-    status: 'normal',
-    lastMaintenance: '2025-03-15',
-    isActive: true,
-    battery: 87,
-    lastCommunication: '2025-04-02 14:23:11',
-    installHistory: {
-      firstInstall: '2021-06-10',
-      lastInstall: '2024-10-20',
-      installCount: 2,
-      previousLocations: ['人民路与中山路口'],
-      remark: '因道路改造迁移'
-    },
-    historyRecords: [
-      { time: '2024-10-20', content: '设备迁移至当前测点人民路与解放路口', type: 'primary' },
-      { time: '2021-06-10', content: '初次安装于人民路与中山路口', type: 'info' }
-    ],
-    metrics: { tilt: 2.3, waterIntrusion: false, gas: 12, vibration: 0.4 }
-  },
-  {
-    deviceId: 'MN-1002',
-    deviceName: '滨江路防爆井盖',
-    deviceType: 'explosion',
-    location: '滨江路化工园区南门',
-    measurePointId: 'MP-B045',
-    status: 'maintenance',
-    lastMaintenance: '2025-02-28',
-    isActive: true,
-    battery: 34,
-    lastCommunication: '2025-04-01 08:52:03',
-    installHistory: {
-      firstInstall: '2022-03-22',
-      lastInstall: '2023-11-05',
-      installCount: 1,
-      previousLocations: [],
-      remark: '高危区域需定期维护'
-    },
-    historyRecords: [
-      { time: '2023-11-05', content: '替换原有普通井盖升级防爆型号', type: 'success' }
-    ],
-    metrics: { tilt: 5.1, waterIntrusion: false, gas: 89, vibration: 1.2 }
-  },
-  {
-    deviceId: 'MN-1003',
-    deviceName: '老城区铸铁井盖',
-    deviceType: 'standard',
-    location: '古城街北段树下',
-    measurePointId: 'MP-C102',
-    status: 'fault',
-    lastMaintenance: '2024-12-10',
-    isActive: true,
-    battery: null,
-    lastCommunication: '2025-03-28 06:15:44',
-    installHistory: {
-      firstInstall: '2018-07-15',
-      lastInstall: '2020-03-01',
-      installCount: 1,
-      previousLocations: ['古城街南段'],
-      remark: '井盖出现沉降'
-    },
-    historyRecords: [
-      { time: '2020-03-01', content: '迁移至古城街北段树下', type: 'warning' }
-    ],
-    metrics: { tilt: 15.2, waterIntrusion: true, gas: 5, vibration: 0.9 }
-  },
-  {
-    deviceId: 'MN-1004',
-    deviceName: '高新区智能井盖',
-    deviceType: 'smart',
-    location: '高新大道云计算中心旁',
-    measurePointId: 'MP-D209',
-    status: 'normal',
-    lastMaintenance: '2025-03-28',
-    isActive: true,
-    battery: 96,
-    lastCommunication: '2025-04-02 16:00:21',
-    installHistory: {
-      firstInstall: '2023-09-18',
-      lastInstall: '2023-09-18',
-      installCount: 1,
-      previousLocations: [],
-      remark: '新建道路配套'
-    },
-    historyRecords: [],
-    metrics: { tilt: 0.8, waterIntrusion: false, gas: 3, vibration: 0.1 }
-  },
-  {
-    deviceId: 'MN-1005',
-    deviceName: '南港路井盖(已停用)',
-    deviceType: 'standard',
-    location: '南港路旧车站段',
-    measurePointId: 'MP-E176',
-    status: 'inactive',
-    lastMaintenance: '2024-06-20',
-    isActive: false,
-    battery: null,
-    lastCommunication: '2024-07-01 02:00:00',
-    installHistory: {
-      firstInstall: '2017-05-02',
-      lastInstall: '2019-12-15',
-      installCount: 2,
-      previousLocations: ['南港路中段', '南港路西段'],
-      remark: '区域改造,设备已封存'
-    },
-    historyRecords: [
-      { time: '2024-08-01', content: '设备停用,进入已用状态', type: 'info' },
-      { time: '2019-12-15', content: '迁移至当前测点安装', type: 'primary' }
-    ],
-    metrics: { tilt: 0, waterIntrusion: false, gas: 0, vibration: 0 }
-  }
-]
+// 窨井设备类别:供水窨井(type_id=5)/ 排水窨井(type_id=9)/ 燃气窨井(type_id=10)
+const MANHOLE_CATEGORY_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
+
+// 设备列表 & 分页
+const devices = ref([])
+const loading = ref(false)
+const total = ref(0)
+const currentPage = ref(1)
+const pageSize = 10
 
-// 状态筛选 & 分页
+// 筛选条件
 const searchKeyword = ref('')
-const statusFilter = ref('')
 const typeFilter = ref('')
-const currentPage = ref(1)
-const pageSize = 5
-
-const filteredDevices = computed(() => {
-  let result = [...mockDevices]
-  // 搜索关键字过滤
-  if (searchKeyword.value) {
-    const kw = searchKeyword.value.toLowerCase()
-    result = result.filter(d =>
-        d.deviceId.toLowerCase().includes(kw) ||
-        d.deviceName.toLowerCase().includes(kw) ||
-        d.location.toLowerCase().includes(kw)
-    )
-  }
-  // 状态过滤
-  if (statusFilter.value) {
-    result = result.filter(d => d.status === statusFilter.value)
-  }
-  // 类型过滤
-  if (typeFilter.value) {
-    result = result.filter(d => d.deviceType === typeFilter.value)
-  }
-  return result
-})
+// 窨井类别选项(value=窨井类别ID:供水窨井5/排水窨井9/燃气窨井10)
+const typeOptions = ref([
+  { value: '5', label: '供水窨井' },
+  { value: '9', label: '排水窨井' },
+  { value: '10', label: '燃气窨井' }
+])
+// 全部类型:id -> { typeId, typeName, parentTypeId }
+const typeIdMap = ref({})
 
-const pagedDevices = computed(() => {
-  const start = (currentPage.value - 1) * pageSize
-  return filteredDevices.value.slice(start, start + pageSize)
-})
+// 统计
+const onlineDevicesCount = ref(0)
+const offlineDevicesCount = ref(0)
+const totalDevicesCount = ref(0)
 
-// 统计卡片数据(基于原始数据)
-const activeDevicesCount = computed(() => mockDevices.filter(d => d.isActive).length)
-const inactiveDevicesCount = computed(() => mockDevices.filter(d => !d.isActive).length)
-const locationsCount = computed(() => new Set(mockDevices.map(d => d.measurePointId)).size)
+// 加载全部设备类型(用于类型列归属显示)
+async function loadTypeOptions() {
+  try {
+    const res = await getEquipmentTypeList()
+    const all = res.data || []
+    all.forEach(t => { typeIdMap.value[t.id] = t })
+  } catch (e) { /* 类型接口异常时,类型列回退显示 typeName */ }
+}
 
-const resetFilters = () => {
-  searchKeyword.value = ''
-  statusFilter.value = ''
-  typeFilter.value = ''
-  currentPage.value = 1
+// 设备类型显示文本(按所属窨井类别显示)
+const getTypeText = (row) => {
+  const t = typeIdMap.value[row.equipmentTypeId]
+  if (!t) return row.typeName || '—'
+  return MANHOLE_CATEGORY_MAP[t.parentTypeId] || MANHOLE_CATEGORY_MAP[row.equipmentTypeId] || t.typeName || '—'
 }
 
-const getStatusType = (status) => {
-  switch(status) {
-    case 'normal': return 'success'
-    case 'maintenance': return 'warning'
-    case 'fault': return 'danger'
-    case 'inactive': return 'info'
-    default: return 'info'
+// 加载设备列表(后端已按窨井设备类型过滤)
+async function loadDevices() {
+  loading.value = true
+  try {
+    const res = await getManholeDevicePage({
+      pageNum: currentPage.value,
+      pageSize,
+      deviceName: searchKeyword.value || null,
+      equipmentTypeId: typeFilter.value || null,
+      isQueryManholeData: true
+    })
+    const rows = res.data?.records || []
+    devices.value = rows.map(d => ({
+      equipmentId: d.equipmentId,
+      equipmentTypeId: d.equipmentTypeId,
+      deviceCode: d.equipmentCode,
+      deviceName: d.equipmentName,
+      typeName: d.equipmentTypeName,
+      location: d.equipmentLocation,
+      onlineStatus: d.equipmentStatus?.onlineStatus,
+      lastMaintainDate: d.equipmentStatus?.lastMaintainDate,
+      batteryLevel: d.manholeData?.batteryLevel,
+      lastCommunication: d.manholeData?.uploadTime || d.manholeData?.createTime,
+      metrics: d.manholeData ? {
+        tilt: d.manholeData.tiltAngle,
+        waterIntrusion: Number(d.manholeData.waterInfiltrationAlarmStatus) === 1,
+        temperature: d.manholeData.temperatureValue,
+        signal: d.manholeData.signalStrength
+      } : null
+    }))
+    total.value = res.data?.total || 0
+  } catch (e) {
+    ElMessage.error('设备列表加载失败')
+  } finally {
+    loading.value = false
   }
 }
-const getStatusText = (status) => {
-  switch(status) {
-    case 'normal': return '正常运行'
-    case 'maintenance': return '需维护'
-    case 'fault': return '故障'
-    case 'inactive': return '停用'
-    default: return '未知'
+
+// 统计(基于当前筛选条件下的全量设备)
+async function loadStats() {
+  try {
+    const res = await getManholeDevicePage({
+      pageNum: 1,
+      pageSize: 1000,
+      deviceName: searchKeyword.value || null,
+      equipmentTypeId: typeFilter.value || null,
+      isQueryManholeData: false
+    })
+    const rows = res.data?.records || []
+    totalDevicesCount.value = res.data?.total || rows.length
+    onlineDevicesCount.value = rows.filter(d => Number(d.equipmentStatus?.onlineStatus) === 1).length
+    offlineDevicesCount.value = rows.length - onlineDevicesCount.value
+  } catch (e) {
+    onlineDevicesCount.value = 0
+    offlineDevicesCount.value = 0
+    totalDevicesCount.value = 0
   }
 }
-const getDeviceTypeTag = (type) => {
-  if (type === 'smart') return 'success'
-  if (type === 'explosion') return 'danger'
-  return ''
+
+// 在线状态
+const getOnlineText = (status) => {
+  return Number(status) === 1 ? '在线' : '离线'
 }
-const getDeviceTypeText = (type) => {
-  if (type === 'smart') return '智能井盖'
-  if (type === 'explosion') return '防爆井盖'
-  return '普通井盖'
+const getOnlineTagType = (status) => {
+  return Number(status) === 1 ? 'success' : 'info'
 }
+
+// 搜索/筛选变化
+const handleSearch = () => {
+  currentPage.value = 1
+  loadDevices()
+  loadStats()
+}
+
+const resetFilters = () => {
+  searchKeyword.value = ''
+  typeFilter.value = ''
+  currentPage.value = 1
+  loadDevices()
+  loadStats()
+}
+
 const tableRowClassName = ({ row }) => {
-  if (row.status === 'fault') return 'warning-row'
-  if (!row.isActive) return 'inactive-row'
+  if (Number(row.onlineStatus) !== 1) return 'inactive-row'
   return ''
 }
 
+// 详情
 const drawerVisible = ref(false)
+const detailLoading = ref(false)
 const selectedDevice = ref(null)
 
-const showDetail = (device) => {
-  selectedDevice.value = device
+// 映射详情接口返回数据
+const mapDeviceDetail = (d) => ({
+  equipmentId: d.equipmentId,
+  equipmentTypeId: d.equipmentTypeId,
+  deviceCode: d.equipmentCode,
+  deviceName: d.equipmentName,
+  typeName: d.equipmentTypeName,
+  location: d.equipmentLocation,
+  onlineStatus: d.equipmentStatus?.onlineStatus,
+  lastMaintainDate: d.equipmentStatus?.lastMaintainDate,
+  batteryLevel: d.manholeData?.batteryLevel,
+  lastCommunication: d.manholeData?.uploadTime || d.manholeData?.createTime,
+  metrics: d.manholeData ? {
+    tilt: d.manholeData.tiltAngle,
+    waterIntrusion: Number(d.manholeData.waterInfiltrationAlarmStatus) === 1,
+    temperature: d.manholeData.temperatureValue,
+    signal: d.manholeData.signalStrength
+  } : null,
+  warningThresholds: d.warningThresholdList || []
+})
+
+const showDetail = async (device) => {
   drawerVisible.value = true
+  detailLoading.value = true
+  try {
+    const res = await getManholeDeviceById(device.equipmentId)
+    selectedDevice.value = res.data ? mapDeviceDetail(res.data) : null
+  } catch (e) {
+    selectedDevice.value = null
+    ElMessage.error('设备详情加载失败')
+  } finally {
+    detailLoading.value = false
+  }
 }
 const handleRowClick = (row) => {
   showDetail(row)
@@ -403,6 +378,115 @@ const handleRowClick = (row) => {
 const closeDrawer = () => {
   drawerVisible.value = false
 }
+
+// ============ 关联设备管理 ============
+const relDialogVisible = ref(false)
+const relDevice = ref(null)
+const relList = ref([])
+const relLoading = ref(false)
+const relSearchKeyword = ref('')
+const searchResults = ref([])
+const relSearching = ref(false)
+const searchSelection = ref([])
+const relSaving = ref(false)
+
+const openRelDialog = async (device) => {
+  relDevice.value = device
+  relDialogVisible.value = true
+  relSearchKeyword.value = ''
+  searchResults.value = []
+  searchSelection.value = []
+  await loadRelList(device.equipmentId)
+  await searchRelDevices()
+}
+
+const loadRelList = async (equipmentId) => {
+  relLoading.value = true
+  try {
+    const res = await getManholeDeviceRels(equipmentId)
+    relList.value = res.data || []
+  } catch (e) {
+    relList.value = []
+    ElMessage.error('关联设备列表加载失败')
+  } finally {
+    relLoading.value = false
+  }
+}
+
+const searchRelDevices = async () => {
+  relSearching.value = true
+  try {
+    const keyword = relSearchKeyword.value.trim()
+    // 排除已关联设备ID + 自身设备ID
+    const excludeIds = [
+      ...relList.value.map(r => r.equipmentId),
+      relDevice.value?.equipmentId
+    ].filter(Boolean).join(',')
+    const res = await searchManholeRelDevices({
+      keyword: keyword || undefined,
+      excludeIds: excludeIds || undefined
+    })
+    searchResults.value = res.data || []
+  } catch (e) {
+    searchResults.value = []
+    ElMessage.error('设备搜索失败')
+  } finally {
+    relSearching.value = false
+  }
+}
+
+const handleSearchSelectionChange = (selection) => {
+  searchSelection.value = selection || []
+}
+
+const batchAddRel = async () => {
+  if (!searchSelection.value.length) return
+  relSaving.value = true
+  try {
+    for (const item of searchSelection.value) {
+      await addManholeDeviceRel({
+        manholeId: relDevice.value.equipmentId,
+        equipmentId: item.equipmentId
+      })
+    }
+    ElMessage.success(`已关联 ${searchSelection.value.length} 台设备`)
+    searchSelection.value = []
+    relSearchKeyword.value = ''
+    await loadRelList(relDevice.value.equipmentId)
+    await searchRelDevices()
+  } catch (e) {
+    const msg = e?.message || e?.msg || '关联失败'
+    ElMessage.error(msg)
+  } finally {
+    relSaving.value = false
+  }
+}
+
+const removeRel = async (row) => {
+  try {
+    await deleteManholeDeviceRel(row.relId)
+    ElMessage.success('已取消关联')
+    await loadRelList(relDevice.value.equipmentId)
+    await searchRelDevices()
+  } catch (e) {
+    const msg = e?.message || e?.msg || '取消关联失败'
+    ElMessage.error(msg)
+  }
+}
+
+const onRelDialogClose = () => {
+  relList.value = []
+  searchResults.value = []
+  searchSelection.value = []
+  relSearchKeyword.value = ''
+}
+
+// 初始化
+onMounted(() => {
+  loadTypeOptions()
+  loadDevices()
+  loadStats()
+})
 </script>
 
 <style scoped>
@@ -418,17 +502,6 @@ const closeDrawer = () => {
   margin-bottom: 20px;
   flex-wrap: wrap;
 }
-.title-section {
-  display: flex;
-  align-items: baseline;
-  gap: 12px;
-}
-.title-section h1 {
-  margin: 0;
-  font-size: 24px;
-  font-weight: 500;
-  color: #1f2d3d;
-}
 .stats-section {
   display: flex;
   gap: 16px;
@@ -478,20 +551,36 @@ const closeDrawer = () => {
   background: white;
   border-top: 1px solid #e4e7ed;
 }
-.expand-detail {
-  padding: 16px 24px;
-  background-color: #fafbfc;
-}
 .monitor-metrics {
   background: #f8f9fc;
   border-radius: 8px;
   padding: 10px;
 }
-:deep(.warning-row) {
-  --el-table-tr-bg-color: #fdf6ec;
-}
 :deep(.inactive-row) {
   --el-table-tr-bg-color: #f5f5f5;
   color: #a8a8a8;
 }
-</style>
+.rel-container {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+.rel-section {
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  padding: 12px;
+}
+.rel-section-title {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 10px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #2c3e50;
+}
+.rel-search-footer {
+  margin-top: 10px;
+  text-align: right;
+}
+</style>

+ 72 - 318
src/views/subSystem/manholeCover/device/statistics.vue

@@ -1,28 +1,6 @@
 <!--监测设备统计分析-->
 <template>
   <div class="device-statistics-dashboard">
-
-
-    <!-- 筛选区域 -->
-    <div class="filter-bar">
-      <div class="filter-group">
-        <span class="filter-label">所属区域:</span>
-        <el-select v-model="selectedRegion" placeholder="全部区域" clearable style="width: 160px">
-          <el-option label="全部区域" value="" />
-          <el-option v-for="region in regionList" :key="region" :label="region" :value="region" />
-        </el-select>
-      </div>
-      <div class="filter-group">
-        <span class="filter-label">设备类型:</span>
-        <el-select v-model="selectedType" placeholder="全部类型" clearable style="width: 160px">
-          <el-option label="全部类型" value="" />
-          <el-option v-for="type in typeList" :key="type" :label="type" :value="type" />
-        </el-select>
-      </div>
-      <el-button type="primary" @click="resetFilters">重置筛选</el-button>
-      <el-button @click="exportData" :icon="Download">导出数据</el-button>
-    </div>
-
     <!-- 图表展示区域:左右两列 -->
     <div class="charts-container">
       <!-- 左侧:区域分布柱状图 -->
@@ -39,9 +17,7 @@
             <el-radio-button label="percent">占比 (%)</el-radio-button>
           </el-radio-group>
         </div>
-        <div class="chart-wrapper">
-          <v-chart :option="regionBarOption" autoresize />
-        </div>
+        <div ref="regionBarRef" class="chart-wrapper"></div>
       </div>
 
       <!-- 右侧:设备类型分布饼图 -->
@@ -58,150 +34,49 @@
             <el-radio-button label="doughnut">环形图</el-radio-button>
           </el-radio-group>
         </div>
-        <div class="chart-wrapper">
-          <v-chart :option="deviceTypePieOption" autoresize />
-        </div>
-      </div>
-    </div>
-
-    <!-- 下方:详细数据表格 -->
-    <div class="data-table-container">
-      <div class="table-header">
-        <span class="card-title">
-          <el-icon>
-            <List />
-          </el-icon>
-          设备明细数据
-        </span>
-        <el-input v-model="searchKeyword" placeholder="搜索设备名称/编号/区域" clearable style="width: 240px" :prefix-icon="Search"
-          size="small" />
-      </div>
-      <el-table :data="paginatedTableData" stripe border style="width: 100%">
-        <el-table-column prop="deviceCode" label="设备编号" width="130" />
-        <el-table-column prop="deviceName" label="设备名称" width="150" show-overflow-tooltip />
-        <el-table-column prop="deviceType" label="设备类型" width="120">
-          <template #default="{ row }">
-            <el-tag :type="getTypeTagColor(row.deviceType)" size="small">{{ row.deviceType }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="region" label="所属地点" width="140" />
-        <el-table-column prop="address" label="详细位置" min-width="180" show-overflow-tooltip />
-        <el-table-column prop="status" label="运行状态" width="100" align="center">
-          <template #default="{ row }">
-            <el-badge :value="row.status === '故障' ? '!' : ''" :type="row.status === '故障' ? 'danger' : 'primary'">
-              <el-tag :type="getStatusType(row.status)" effect="plain" size="small">{{ row.status }}</el-tag>
-            </el-badge>
-          </template>
-        </el-table-column>
-        <el-table-column prop="installDate" label="安装日期" width="120" />
-      </el-table>
-      <div class="pagination-wrapper">
-        <el-pagination background layout="total, prev, pager, next" :total="filteredTableData.length"
-          :page-size="pageSize" v-model:current-page="currentPage" v-model:page-size="pageSize"
-          :page-sizes="[5, 10, 20]" />
+        <div ref="typePieRef" class="chart-wrapper"></div>
       </div>
     </div>
   </div>
 </template>
 
 <script setup>
-import { ref, computed, watch } from 'vue'
-import { Location, PieChart, List, Search, Download } from '@element-plus/icons-vue'
-import { use } from 'echarts/core'
-import { CanvasRenderer } from 'echarts/renderers'
-import { BarChart, PieChart as EChartsPie } from 'echarts/charts'
-import {
-  TitleComponent,
-  TooltipComponent,
-  LegendComponent,
-  GridComponent,
-  DatasetComponent,
-  TransformComponent
-} from 'echarts/components'
-
-// 注册 ECharts 组件
-use([
-  CanvasRenderer,
-  BarChart,
-  EChartsPie,
-  TitleComponent,
-  TooltipComponent,
-  LegendComponent,
-  GridComponent,
-  DatasetComponent,
-  TransformComponent
-])
-
-// ---------- 模拟设备数据 ----------
-const mockDevices = [
-  { deviceCode: 'DEV-1001', deviceName: '智能水位监测仪', deviceType: '水位计', region: '浦东新区', address: '世纪大道100号', status: '正常', installDate: '2024-03-15' },
-  { deviceCode: 'DEV-1002', deviceName: '井盖状态传感器', deviceType: '智能井盖', region: '浦东新区', address: '张江高科技园区', status: '正常', installDate: '2024-05-20' },
-  { deviceCode: 'DEV-1003', deviceName: '流量监测终端', deviceType: '流量计', region: '徐汇区', address: '漕溪北路88号', status: '维护', installDate: '2023-11-02' },
-  { deviceCode: 'DEV-1004', deviceName: '水质分析仪', deviceType: '水质仪', region: '徐汇区', address: '龙吴路1500号', status: '正常', installDate: '2024-01-10' },
-  { deviceCode: 'DEV-1005', deviceName: '智能井盖终端', deviceType: '智能井盖', region: '黄浦区', address: '人民大道200号', status: '故障', installDate: '2023-09-18' },
-  { deviceCode: 'DEV-1006', deviceName: '压力传感器', deviceType: '压力计', region: '黄浦区', address: '外马路99号', status: '正常', installDate: '2024-02-28' },
-  { deviceCode: 'DEV-1007', deviceName: '多参数水质仪', deviceType: '水质仪', region: '静安区', address: '南京西路1266号', status: '正常', installDate: '2024-04-12' },
-  { deviceCode: 'DEV-1008', deviceName: '智慧井盖监测器', deviceType: '智能井盖', region: '静安区', address: '万荣路700号', status: '正常', installDate: '2024-06-01' },
-  { deviceCode: 'DEV-1009', deviceName: '超声波流量计', deviceType: '流量计', region: '浦东新区', address: '金桥路1851号', status: '维护', installDate: '2023-12-10' },
-  { deviceCode: 'DEV-1010', deviceName: '遥测水位计', deviceType: '水位计', region: '杨浦区', address: '淞沪路388号', status: '正常', installDate: '2024-03-20' },
-  { deviceCode: 'DEV-1011', deviceName: '智能井盖传感模块', deviceType: '智能井盖', region: '杨浦区', address: '军工路516号', status: '故障', installDate: '2023-10-05' },
-  { deviceCode: 'DEV-1012', deviceName: '气体监测仪', deviceType: '气体仪', region: '闵行区', address: '剑川路930号', status: '正常', installDate: '2024-05-16' },
-  { deviceCode: 'DEV-1013', deviceName: '液位传感器', deviceType: '水位计', region: '闵行区', address: '东川路800号', status: '正常', installDate: '2024-01-25' },
-  { deviceCode: 'DEV-1014', deviceName: '防爆智能井盖', deviceType: '智能井盖', region: '浦东新区', address: '临港新城主城区', status: '正常', installDate: '2024-07-08' },
-  { deviceCode: 'DEV-1015', deviceName: '管网压力监测', deviceType: '压力计', region: '徐汇区', address: '虹漕路421号', status: '维护', installDate: '2023-08-30' },
-]
-
-// 提取区域列表和设备类型列表(用于筛选)
-const regionList = computed(() => [...new Set(mockDevices.map(d => d.region))])
-const typeList = computed(() => [...new Set(mockDevices.map(d => d.deviceType))])
-
-// 筛选条件
-const selectedRegion = ref('')
-const selectedType = ref('')
-const searchKeyword = ref('')
-const currentPage = ref(1)
-const pageSize = ref(10)
+import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
+import { Location, PieChart } from '@element-plus/icons-vue'
+import { getManholeDeviceStatistics } from '@/api/pipeNetwork/basic'
+import { ElMessage } from 'element-plus'
+import * as echarts from 'echarts'
 
 // 图表显示模式
 const barChartType = ref('count')   // count / percent
 const pieChartType = ref('normal')  // normal / doughnut
 
-// 计算设备总数
-const totalDevices = computed(() => mockDevices.length)
-const regionCount = computed(() => regionList.value.length)
-const deviceTypeCount = computed(() => typeList.value.length)
-
-// 获取区域分布数据(基于筛选后的设备,但柱状图展示全局分布,为了直观,默认展示全部数据,但可考虑筛选联动)
-// 这里设计:区域分布图展示全局设备按区域分布,不做筛选联动即可体现整体分布。
-const regionDistribution = computed(() => {
-  const regionMap = new Map()
-  mockDevices.forEach(device => {
-    const region = device.region
-    regionMap.set(region, (regionMap.get(region) || 0) + 1)
-  })
-  return Array.from(regionMap.entries())
-    .map(([region, count]) => ({ region, count }))
-    .sort((a, b) => b.count - a.count)
-})
-
-// 设备类型分布
-const typeDistribution = computed(() => {
-  const typeMap = new Map()
-  mockDevices.forEach(device => {
-    const type = device.deviceType
-    typeMap.set(type, (typeMap.get(type) || 0) + 1)
-  })
-  return Array.from(typeMap.entries())
-    .map(([type, count]) => ({ type, count }))
-    .sort((a, b) => b.count - a.count)
-})
+// 统计数据(来自接口)
+const regionDistribution = ref([])
+const typeDistribution = ref([])
+
+// 图表实例
+let regionBarChart = null
+let typePieChart = null
+const regionBarRef = ref(null)
+const typePieRef = ref(null)
+
+// 加载统计数据(默认全部窨井设备)
+async function loadStatistics() {
+  try {
+    const res = await getManholeDeviceStatistics({})
+    regionDistribution.value = res.data?.regionDistribution || []
+    typeDistribution.value = res.data?.typeDistribution || []
+  } catch (e) {
+    ElMessage.error('统计数据加载失败')
+  }
+}
 
-// 柱状图配置
+// 柱状图配置:设备区域分布(数量/占比)
 const regionBarOption = computed(() => {
-  const regions = regionDistribution.value.map(item => item.region)
+  const regions = regionDistribution.value.map(item => item.name)
   const counts = regionDistribution.value.map(item => item.count)
-  const total = counts.reduce((a, b) => a + b, 0)
-  const percentages = counts.map(c => ((c / total) * 100).toFixed(1))
+  const percentages = regionDistribution.value.map(item => item.percent)
 
   const seriesData = barChartType.value === 'count' ? counts : percentages
   const yAxisName = barChartType.value === 'count' ? '设备数量 (台)' : '占比 (%)'
@@ -257,13 +132,12 @@ const regionBarOption = computed(() => {
   }
 })
 
-// 饼图配置
-const deviceTypePieOption = computed(() => {
+// 饼图配置:设备类型分布(标准/环形)
+const typePieOption = computed(() => {
   const pieData = typeDistribution.value.map(item => ({
-    name: item.type,
+    name: item.name,
     value: item.count
   }))
-  const total = pieData.reduce((sum, d) => sum + d.value, 0)
 
   return {
     tooltip: {
@@ -302,71 +176,36 @@ const deviceTypePieOption = computed(() => {
   }
 })
 
-// 表格数据筛选 (支持区域、类型、关键词)
-const filteredTableData = computed(() => {
-  let data = [...mockDevices]
-  if (selectedRegion.value) {
-    data = data.filter(d => d.region === selectedRegion.value)
-  }
-  if (selectedType.value) {
-    data = data.filter(d => d.deviceType === selectedType.value)
-  }
-  if (searchKeyword.value) {
-    const kw = searchKeyword.value.toLowerCase()
-    data = data.filter(d =>
-      d.deviceCode.toLowerCase().includes(kw) ||
-      d.deviceName.toLowerCase().includes(kw) ||
-      d.region.toLowerCase().includes(kw)
-    )
-  }
-  return data
-})
+// 图表配置变化时刷新
+watch([regionBarOption, typePieOption], () => {
+  if (regionBarChart) regionBarChart.setOption(regionBarOption.value, true)
+  if (typePieChart) typePieChart.setOption(typePieOption.value, true)
+}, { deep: true })
+
+// 窗口变化自适应(防抖)
+let resizeTimer = null
+const handleResize = () => {
+  clearTimeout(resizeTimer)
+  resizeTimer = setTimeout(() => {
+    regionBarChart && regionBarChart.resize()
+    typePieChart && typePieChart.resize()
+  }, 100)
+}
 
-const paginatedTableData = computed(() => {
-  const start = (currentPage.value - 1) * pageSize.value
-  return filteredTableData.value.slice(start, start + pageSize.value)
+onMounted(async () => {
+  await nextTick()
+  regionBarChart = echarts.init(regionBarRef.value)
+  typePieChart = echarts.init(typePieRef.value)
+  window.addEventListener('resize', handleResize)
+  loadStatistics()
 })
 
-// 监听筛选变化重置页码
-watch([selectedRegion, selectedType, searchKeyword], () => {
-  currentPage.value = 1
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', handleResize)
+  clearTimeout(resizeTimer)
+  regionBarChart && regionBarChart.dispose()
+  typePieChart && typePieChart.dispose()
 })
-
-const resetFilters = () => {
-  selectedRegion.value = ''
-  selectedType.value = ''
-  searchKeyword.value = ''
-}
-
-const exportData = () => {
-  // 简单导出CSV
-  const headers = ['设备编号', '设备名称', '设备类型', '所属地点', '详细位置', '运行状态', '安装日期']
-  const rows = filteredTableData.value.map(d => [
-    d.deviceCode, d.deviceName, d.deviceType, d.region, d.address, d.status, d.installDate
-  ])
-  const csvContent = [headers, ...rows].map(row => row.join(',')).join('\n')
-  const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' })
-  const link = document.createElement('a')
-  const url = URL.createObjectURL(blob)
-  link.href = url
-  link.setAttribute('download', '设备统计报表.csv')
-  document.body.appendChild(link)
-  link.click()
-  document.body.removeChild(link)
-  URL.revokeObjectURL(url)
-}
-
-const getTypeTagColor = (type) => {
-  const map = { '智能井盖': 'success', '水位计': 'primary', '流量计': 'warning', '水质仪': 'danger', '压力计': 'info', '气体仪': '' }
-  return map[type] || ''
-}
-
-const getStatusType = (status) => {
-  if (status === '正常') return 'success'
-  if (status === '维护') return 'warning'
-  if (status === '故障') return 'danger'
-  return 'info'
-}
 </script>
 
 <style scoped>
@@ -374,78 +213,17 @@ const getStatusType = (status) => {
   padding: 20px;
   background: #f5f7fb;
   min-height: 100vh;
+  box-sizing: border-box;
   font-family: 'Segoe UI', 'PingFang SC', Roboto, Helvetica, Arial, sans-serif;
 }
 
-.dashboard-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  background: white;
-  padding: 16px 24px;
-  border-radius: 16px;
-  margin-bottom: 20px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
-}
-
-.title-section h2 {
-  margin: 0 0 4px 0;
-  font-size: 22px;
-  font-weight: 600;
-  color: #1f2d3d;
-}
-
-.header-stats {
-  display: flex;
-  gap: 32px;
-}
-
-.stat-item {
-  text-align: center;
-}
-
-.stat-value {
-  font-size: 28px;
-  font-weight: 700;
-  color: #409eff;
-  line-height: 1.2;
-}
-
-.stat-label {
-  font-size: 13px;
-  color: #909399;
-  margin-top: 4px;
-}
-
-.filter-bar {
-  background: white;
-  padding: 12px 20px;
-  border-radius: 12px;
-  margin-bottom: 20px;
-  display: flex;
-  align-items: center;
-  gap: 16px;
-  flex-wrap: wrap;
-  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
-}
-
-.filter-group {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-}
-
-.filter-label {
-  font-size: 14px;
-  color: #606266;
-  font-weight: 500;
-}
-
 .charts-container {
   display: grid;
   grid-template-columns: 1fr 1fr;
   gap: 20px;
-  margin-bottom: 24px;
+  /* 随视口高度自适应,占满剩余空间 */
+  height: calc(100vh - 40px);
+  min-height: 420px;
 }
 
 .chart-card {
@@ -453,7 +231,9 @@ const getStatusType = (status) => {
   border-radius: 16px;
   box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
   overflow: hidden;
-  transition: all 0.2s;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
 }
 
 .card-header {
@@ -462,6 +242,7 @@ const getStatusType = (status) => {
   align-items: center;
   padding: 16px 20px;
   border-bottom: 1px solid #ebeef5;
+  flex-shrink: 0;
 }
 
 .card-title {
@@ -473,37 +254,10 @@ const getStatusType = (status) => {
   color: #303133;
 }
 
+/* 图表容器占满卡片剩余高度 */
 .chart-wrapper {
-  padding: 16px;
-  height: 380px;
-}
-
-.data-table-container {
-  background: white;
-  border-radius: 16px;
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
-  padding: 0 16px 16px 16px;
-}
-
-.table-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 16px 0 12px 0;
-  border-bottom: 1px solid #e9ecef;
-  margin-bottom: 12px;
-}
-
-.pagination-wrapper {
-  display: flex;
-  justify-content: flex-end;
-  margin-top: 16px;
-  padding-top: 8px;
-}
-
-:deep(.el-table th) {
-  background-color: #f8fafc;
-  font-weight: 600;
-  color: #2c3e50;
+  flex: 1;
+  min-height: 0;
+  width: 100%;
 }
-</style>
+</style>

+ 523 - 145
src/views/subSystem/manholeCover/home/index.vue

@@ -1,49 +1,56 @@
+<!--窨井盖监测首页大屏-->
 <template>
   <div class="monitor-container">
     <!-- 左侧栏 -->
     <div class="left-col">
       <!-- 待办工单 -->
       <div class="panel">
-        <h3 class="panel-title">待办工单</h3>
-        <el-table :data="workOrders" class="panel-table" :header-cell-style="tableHeaderStyle">
-          <el-table-column prop="orderNo" label="工单编号" />
-          <el-table-column prop="type" label="工单类型" />
-          <el-table-column prop="device" label="设备名称" />
-          <el-table-column prop="priority" label="优先级">
+        <h3 class="panel-title">待办工单 <span class="title-count">{{ workOrders.length }} 条</span></h3>
+        <el-table :data="workOrders" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.work">
+          <el-table-column prop="orderNo" label="工单编号" min-width="120" show-overflow-tooltip />
+          <el-table-column label="类型" width="80">
+            <template #default="{ row }">{{ orderTypeText(row.orderType) }}</template>
+          </el-table-column>
+          <el-table-column prop="deviceCode" label="设备" min-width="100" show-overflow-tooltip />
+          <el-table-column label="优先级" width="70">
             <template #default="{ row }">
-              <span :class="['priority-tag', row.priority.toLowerCase()]">{{ row.priority }}</span>
+              <span :class="['priority-tag', priorityClass(row.orderLevel)]">{{ priorityText(row.orderLevel) }}</span>
             </template>
           </el-table-column>
         </el-table>
       </div>
 
-      <!-- 告警类型统计 -->
+      <!-- 告警类型统计(15天内) -->
       <div class="panel">
         <h3 class="panel-title">告警类型统计(15天内)</h3>
-        <div ref="alarmChartRef" class="chart" />
+        <div ref="alarmChartRef" class="chart" v-loading="loading.alarm" />
       </div>
 
       <!-- 设备区域信息 -->
       <div class="panel">
         <h3 class="panel-title">设备区域信息</h3>
-        <div ref="areaChartRef" class="chart" />
+        <div ref="areaChartRef" class="chart" v-loading="loading.device" />
       </div>
     </div>
 
     <!-- 中间地图区域 -->
     <div class="center-col">
       <div class="panel map-panel">
-        <!-- 这里可以替换为真实地图(如高德/百度地图) -->
-        <div class="map-placeholder">
-          <p>地图区域(可接入真实地图)</p>
-          <!-- 示例路径/点位示意,后续可通过地图API绘制 -->
+        <div class="map-header">
+          <span>窨井设备分布</span>
+          <div class="map-legend">
+            <span><i class="legend-dot online"></i> 在线 {{ stats.online }}</span>
+            <span><i class="legend-dot alert"></i> 告警 {{ stats.alarm }}</span>
+            <span><i class="legend-dot offline"></i> 离线 {{ stats.offline }}</span>
+          </div>
         </div>
+        <div id="homeMap" class="bmap-container"></div>
       </div>
 
       <!-- 设备分类 -->
-      <div class="panel">
+      <div class="panel chart-panel">
         <h3 class="panel-title">设备分类</h3>
-        <div ref="deviceTypeChartRef" class="chart" />
+        <div ref="deviceTypeChartRef" class="chart" v-loading="loading.device" />
       </div>
     </div>
 
@@ -51,26 +58,28 @@
     <div class="right-col">
       <!-- 设备列表 -->
       <div class="panel">
-        <h3 class="panel-title">设备列表</h3>
-        <el-table :data="deviceList" class="panel-table" :header-cell-style="tableHeaderStyle">
-          <el-table-column prop="name" label="设备名称" />
-          <el-table-column prop="type" label="设备类型">
-            <template #header>
-              <div>
-                <span>设备类型</span>
-                <el-select v-model="deviceTypeFilter" size="small" style="margin-left: 8px">
-                  <el-option label="全部" value="全部" />
-                  <el-option label="电用井盖" value="电用井盖" />
-                  <el-option label="供水井盖" value="供水井盖" />
-                  <el-option label="通信井盖" value="通信井盖" />
-                  <el-option label="污水井盖" value="污水井盖" />
-                </el-select>
-              </div>
+        <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-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>
+          </el-table-column>
+          <el-table-column label="电量" width="60">
+            <template #default="{ row }">
+              <el-tag :type="batteryTag(row.battery)" size="small">{{ row.battery ?? '-' }}%</el-tag>
             </template>
           </el-table-column>
-          <el-table-column prop="battery" label="设备电量">
+          <el-table-column label="状态" width="60">
             <template #default="{ row }">
-              <el-tag type="success">{{ row.battery }}%</el-tag>
+              <span :class="['status-dot', row.status]"></span>
+              <span class="status-text">{{ statusText(row.status) }}</span>
             </template>
           </el-table-column>
         </el-table>
@@ -79,12 +88,12 @@
       <!-- 动态信息 -->
       <div class="panel">
         <h3 class="panel-title">动态信息</h3>
-        <el-table :data="dynamicLogs" class="panel-table" :header-cell-style="tableHeaderStyle">
-          <el-table-column prop="time" label="时间" />
-          <el-table-column prop="event" label="事件" />
-          <el-table-column prop="status" label="状态">
+        <el-table :data="dynamicLogs" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.alarm">
+          <el-table-column prop="time" label="时间" width="140" />
+          <el-table-column prop="event" label="事件" min-width="120" show-overflow-tooltip />
+          <el-table-column label="状态" width="70">
             <template #default="{ row }">
-              <span :class="['status-tag', row.statusClass]">{{ row.status }}</span>
+              <span :class="['status-tag', row.alarmStatus === 1 ? 'done' : 'processing']">{{ row.alarmStatus === 1 ? '已处理' : '未处理' }}</span>
             </template>
           </el-table-column>
         </el-table>
@@ -94,189 +103,558 @@
 </template>
 
 <script setup>
-import { ref, onMounted } from 'vue'
+import { ref, reactive, onMounted, onBeforeUnmount, nextTick, computed } from 'vue'
 import * as echarts from 'echarts'
+import { getManholeLayerData, getManholeWorkOrderProcessingPage } from '@/api/pipeNetwork/basic'
+import { getAlarmDataPage } from '@/api/drainage'
+import { ElMessage } from 'element-plus'
+import locationIcon from '@/assets/images/location.png'
 
-// 表格表头样式
-const tableHeaderStyle = {
-  background: 'rgba(0, 100, 200, 0.3)',
-  color: '#fff',
-  borderColor: '#2b4b8c'
-}
-
-// 模拟数据
-const workOrders = ref([
-  { orderNo: 'WO202309...', type: '故障维修', device: '井盖一', priority: '高' },
-  { orderNo: 'WO202309...', type: '故障维修', device: '井盖二', priority: '低' },
-  { orderNo: 'WO202309...', type: '故障维修', device: '井盖三', priority: '低' },
-  { orderNo: 'WO202309...', type: '故障维修', device: '井盖四', priority: '低' },
-  { orderNo: 'WO202309...', type: '故障维修', device: '井盖五', priority: '中' }
-])
+// ==================== 状态 ====================
+const loading = reactive({ work: false, alarm: false, device: false })
+const stats = reactive({ total: 0, online: 0, offline: 0, alarm: 0 })
 
-const deviceTypeFilter = ref('全部')
-const deviceList = ref([
-  { name: '井盖一', type: '电用井盖', battery: 80 },
-  { name: '井盖二', type: '供水井盖', battery: 80 },
-  { name: '井盖三', type: '通信井盖', battery: 80 },
-  { name: '井盖四', type: '污水井盖', battery: 80 },
-  { name: '井盖五', type: '污水井盖', battery: 80 },
-  { name: '井盖六', type: '污水井盖', battery: 80 },
-  { name: '井盖七', type: '通信井盖', battery: 80 },
-  { name: '井盖八', type: '通信井盖', battery: 80 },
-  { name: '井盖九', type: '通信井盖', battery: 80 },
-  { name: '井盖十', type: '通信井盖', battery: 80 },
-  { name: '井盖十一', type: '通信井盖', battery: 80 }
-])
+const workOrders = ref([])
+const allDevices = ref([])
+const dynamicLogs = ref([])
+const deviceTypeFilter = ref('')
 
-const dynamicLogs = ref([
-  { time: '2025-09-23 09:23', event: '污水井盖#W023发生倾斜告警', status: '处理中', statusClass: 'processing' },
-  { time: '2025-08-25 09:23', event: '电力井盖#D112维修完成', status: '已完成', statusClass: 'done' },
-  { time: '2025-07-18 09:23', event: '通信井盖#T056电压异常', status: '处理中', statusClass: 'processing' },
-  { time: '2025-08-23 09:23', event: '雨水井盖#Y091更换完成', status: '已完成', statusClass: 'done' }
-])
-
-// 图表DOM引用
+// 图表引用
 const alarmChartRef = ref(null)
 const areaChartRef = ref(null)
 const deviceTypeChartRef = ref(null)
+let alarmChart = null, areaChart = null, deviceTypeChart = null
+
+// ==================== 常量映射 ====================
+const TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
+const tableHeaderStyle = { background: 'rgba(0,100,200,0.3)', color: '#fff', borderColor: '#2b4b8c' }
+
+const orderTypeText = (t) => ({ 1: '故障维修', 2: '日常巡检', 3: '设备保养' }[t] || '-')
+const priorityText = (l) => ({ 1: '紧急', 2: '一般', 3: '低' }[l] || '-')
+const priorityClass = (l) => ({ 1: 'high', 2: 'medium', 3: 'low' }[l] || 'low')
+const typeText = (id) => TYPE_MAP[id] || '-'
+const statusText = (s) => ({ normal: '在线', alert: '告警', offline: '离线' }[s] || s)
+const batteryTag = (b) => {
+  if (b == null) return 'info'
+  if (Number(b) <= 20) return 'danger'
+  if (Number(b) <= 50) return 'warning'
+  return 'success'
+}
+
+// ==================== 筛选后设备列表 ====================
+const filteredDevices = computed(() => {
+  if (!deviceTypeFilter.value) return allDevices.value
+  return allDevices.value.filter(d => d.typeId === deviceTypeFilter.value)
+})
+
+// ==================== 数据加载 ====================
+async function loadAll() {
+  await Promise.allSettled([loadDevices(), loadWorkOrders(), loadAlarmData()])
+  await nextTick()
+  handleResize()
+}
+
+async function loadDevices() {
+  loading.device = true
+  try {
+    const res = await getManholeLayerData({ isQueryManholeData: true })
+    const devices = res.data?.devices || []
+    allDevices.value = devices.map(d => {
+      const onlineStatus = Number(d.equipmentStatus?.onlineStatus)
+      const alarmStatus = Number(d.equipmentStatus?.alarmStatus)
+      const manholeAlarm = Number(d.manholeData?.alarmStatus)
+      const isAlert = alarmStatus === 1 || manholeAlarm === 1 ||
+        Number(d.manholeData?.waterInfiltrationAlarmStatus) === 1 ||
+        Number(d.manholeData?.waterLevelAlarmStatus) === 1
+      let status = 'offline'
+      if (isAlert) status = 'alert'
+      else if (onlineStatus === 1) status = 'normal'
+      return {
+        id: d.equipmentId, code: d.equipmentCode, name: d.equipmentName,
+        typeId: d.equipmentTypeId, typeName: d.equipmentTypeName,
+        location: d.equipmentLocation, district: d.district,
+        lng: d.longitude != null ? Number(d.longitude) : null,
+        lat: d.latitude != null ? Number(d.latitude) : null,
+        battery: d.manholeData?.batteryLevel ?? null,
+        status
+      }
+    })
+    const statistics = res.data?.statistics || {}
+    stats.total = statistics.total || allDevices.value.length
+    stats.online = statistics.online || 0
+    stats.offline = statistics.offline || 0
+    stats.alarm = statistics.alarm || 0
+    renderAreaChart()
+    renderDeviceTypeChart()
+    refreshMarkers()
+  } catch (e) {
+    ElMessage.error('设备数据加载失败')
+  } finally {
+    loading.device = false
+  }
+}
+
+async function loadWorkOrders() {
+  loading.work = true
+  try {
+    const res = await getManholeWorkOrderProcessingPage({ pageNum: 1, pageSize: 20 })
+    workOrders.value = res.data?.records || []
+  } catch (e) {
+    ElMessage.error('工单数据加载失败')
+  } finally {
+    loading.work = false
+  }
+}
+
+async function loadAlarmData() {
+  loading.alarm = true
+  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.alarmTime || r.createTime)
+      return t >= fifteenDaysAgo
+    })
+    // 告警类型统计
+    const typeMap = {}
+    recentAlarms.forEach(r => {
+      const type = r.warningType || '其他'
+      typeMap[type] = (typeMap[type] || 0) + 1
+    })
+    renderAlarmChart(typeMap)
+    // 动态信息(最新10条)
+    dynamicLogs.value = records.slice(0, 10).map(r => ({
+      time: r.alarmTime || r.createTime || '',
+      event: `${r.warningType || '告警'} - ${r.deviceName || r.deviceCode || ''}`,
+      alarmStatus: r.alarmStatus ?? 0
+    }))
+  } catch (e) {
+    ElMessage.error('告警数据加载失败')
+  } finally {
+    loading.alarm = false
+  }
+}
 
-onMounted(() => {
-  // 告警类型统计饼图
-  const alarmChart = echarts.init(alarmChartRef.value)
+// ==================== ECharts ====================
+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) => ({
+    name, value, itemStyle: { color: colors[i % colors.length] }
+  }))
   alarmChart.setOption({
-    tooltip: { trigger: 'item' },
-    legend: { show: false },
+    backgroundColor: 'transparent',
+    tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
+    legend: { bottom: 0, textStyle: { color: '#ccc', fontSize: 10 } },
     series: [{
-      type: 'pie',
-      radius: ['40%', '70%'],
-      data: [
-        { value: 10, name: '位移告警', itemStyle: { color: '#ff3333' } },
-        { value: 15, name: '倾斜告警', itemStyle: { color: '#ffb333' } },
-        { value: 20, name: '水位告警', itemStyle: { color: '#33cc33' } },
-        { value: 5, name: '电压告警', itemStyle: { color: '#3399ff' } },
-        { value: 60, name: '温度告警', itemStyle: { color: '#9966ff' } }
-      ]
+      type: 'pie', radius: ['35%', '60%'], center: ['50%', '40%'],
+      label: { color: '#ccc', fontSize: 10 },
+      data: data.length ? data : [{ name: '暂无数据', value: 0 }]
     }]
   })
+}
 
-  // 设备区域信息柱状图
-  const areaChart = echarts.init(areaChartRef.value)
+function renderAreaChart() {
+  if (!areaChartRef.value) return
+  if (!areaChart) areaChart = echarts.init(areaChartRef.value)
+  const areaMap = {}
+  allDevices.value.forEach(d => {
+    const area = d.district || d.location || '未分配区域'
+    // 取区域名(截取前4个字)
+    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)
   areaChart.setOption({
-    grid: { left: '10%', right: '10%', bottom: '15%', top: '10%' },
+    backgroundColor: 'transparent',
+    tooltip: { trigger: 'axis' },
+    grid: { left: '12%', right: '8%', bottom: '18%', top: '8%' },
     xAxis: {
-      type: 'category',
-      data: ['太安社区', '鸳鸯山社区', '鹤鸣山社区', '廻龙山社区', '凤凰山社区', '老鸦溪社区'],
-      axisLabel: { color: '#fff', interval: 0, rotate: 30 }
+      type: 'category', data: entries.map(e => e[0]),
+      axisLabel: { color: '#ccc', fontSize: 10, interval: 0, rotate: 30 }
     },
-    yAxis: { type: 'value', axisLabel: { color: '#fff' }, splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } } },
-    series: [{
-      type: 'bar',
-      data: [80, 85, 60, 45, 20, 88],
-      itemStyle: { color: '#3399ff' }
-    }]
+    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' } }]
   })
+}
 
-  // 设备分类柱状图
-  const deviceTypeChart = echarts.init(deviceTypeChartRef.value)
+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']
   deviceTypeChart.setOption({
-    grid: { left: '10%', right: '10%', bottom: '15%', top: '10%' },
+    backgroundColor: 'transparent',
+    tooltip: { trigger: 'axis' },
+    grid: { left: '12%', right: '8%', bottom: '15%', top: '8%' },
     xAxis: {
-      type: 'category',
-      data: ['电用井盖', '通信井盖', '污水井盖', '路灯井盖', '电缆井盖', '化粪池井盖'],
-      axisLabel: { color: '#fff', interval: 0, rotate: 30 }
+      type: 'category', data: labels,
+      axisLabel: { color: '#ccc', fontSize: 11, interval: 0 }
     },
-    yAxis: { type: 'value', axisLabel: { color: '#fff' }, splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } } },
+    yAxis: { type: 'value', axisLabel: { color: '#ccc' }, splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } } },
     series: [{
-      type: 'bar',
-      data: [120, 80, 60, 30, 25, 20],
-      itemStyle: { color: '#3399ff' }
+      type: 'bar', data: values.map((v, i) => ({ value: v, itemStyle: { color: colors[i] } })),
+      barWidth: '40%', label: { show: true, position: 'top', color: '#ccc' }
     }]
   })
+}
+
+// ==================== 百度地图 ====================
+let mapInstance = null
+const markerMap = new Map()
+const YUANLING_CENTER = { lng: 110.393, lat: 28.452 }
+
+function initMap() {
+  const container = document.getElementById('homeMap')
+  if (!container || typeof BMapGL === 'undefined') return
+  try {
+    mapInstance = new BMapGL.Map('homeMap')
+    mapInstance.centerAndZoom(new BMapGL.Point(YUANLING_CENTER.lng, YUANLING_CENTER.lat), 13)
+    mapInstance.enableScrollWheelZoom(true)
+  } catch (e) { console.error('地图初始化失败', e) }
+}
+
+function getStatusColor(status) {
+  if (status === 'alert') return '#F56C6C'
+  if (status === 'offline') return '#909399'
+  return '#67C23A'
+}
+
+function refreshMarkers() {
+  if (!mapInstance) return
+  markerMap.forEach(label => mapInstance.removeOverlay(label))
+  markerMap.clear()
+  allDevices.value.forEach(d => {
+    if (d.lng == null || d.lat == null) return
+    const color = getStatusColor(d.status)
+    const html = `<div style="text-align:center;cursor:pointer;">
+      <div style="color:#fff;background:${color};border-radius:3px;padding:1px 6px;font-size:10px;white-space:nowrap;display:inline-block;margin-bottom:2px;box-shadow:0 1px 3px rgba(0,0,0,0.3);">${d.name}</div>
+      <div><img src="${locationIcon}" 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),
+      offset: new BMapGL.Size(-25, -40)
+    })
+    label.setStyle({ border: 'none', background: 'transparent', padding: '0', zIndex: '10' })
+    mapInstance.addOverlay(label)
+    markerMap.set(d.id, label)
+  })
+}
+
+// ==================== 生命周期 ====================
+const handleResize = () => {
+  alarmChart && alarmChart.resize()
+  areaChart && areaChart.resize()
+  deviceTypeChart && deviceTypeChart.resize()
+  mapInstance && mapInstance.resize && mapInstance.resize()
+}
+
+onMounted(async () => {
+  await nextTick()
+  initMap()
+  window.addEventListener('resize', handleResize)
+  loadAll()
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', handleResize)
+  alarmChart && alarmChart.dispose()
+  areaChart && areaChart.dispose()
+  deviceTypeChart && deviceTypeChart.dispose()
+  if (mapInstance) {
+    mapInstance.clearOverlays && mapInstance.clearOverlays()
+    mapInstance = null
+  }
+  markerMap.clear()
 })
 </script>
 
+<!-- ==================== 全局样式(非 scoped):el-table 深色主题覆盖 ==================== -->
+<!-- 用 .monitor-container 限定范围,不影响其他页面 -->
+<style>
+.monitor-container .el-table {
+  --el-table-bg-color: transparent;
+  --el-table-tr-bg-color: transparent;
+  --el-table-header-bg-color: transparent;
+  --el-table-current-row-bg-color: rgba(0, 100, 200, 0.25);
+  --el-table-row-hover-bg-color: rgba(0, 100, 200, 0.15);
+  --el-table-text-color: #dce3ee;
+  --el-table-header-text-color: #fff;
+  --el-table-border-color: rgba(43, 75, 140, 0.35);
+  --el-table-border: 1px solid rgba(43, 75, 140, 0.35);
+  --el-fill-color-lighter: rgba(30, 40, 90, 0.25);
+  --el-fill-color-light: rgba(30, 40, 90, 0.25);
+  --el-fill-color: rgba(30, 40, 90, 0.25);
+  --el-fill-color-blank: transparent;
+  --el-text-color-regular: #dce3ee;
+  --el-text-color-secondary: #9ba8c0;
+  --el-border-color: rgba(43, 75, 140, 0.35);
+  --el-border-color-lighter: rgba(43, 75, 140, 0.2);
+  background: transparent !important;
+  border: none !important;
+}
+/* 隐藏 EP 表格自带的底部/右侧边框伪元素 */
+.monitor-container .el-table::before,
+.monitor-container .el-table::after,
+.monitor-container .el-table .el-table__inner-wrapper::before,
+.monitor-container .el-table .el-table__inner-wrapper::after {
+  display: none !important;
+}
+/* 内部容器全透明 */
+.monitor-container .el-table .el-table__inner-wrapper,
+.monitor-container .el-table .el-table__body-wrapper,
+.monitor-container .el-table .el-table__header-wrapper,
+.monitor-container .el-table .el-table__body,
+.monitor-container .el-table .el-table__header {
+  background: transparent !important;
+}
+/* 表头单元格 — EP 2.x 使用 th.el-table__cell */
+.monitor-container .el-table th.el-table__cell {
+  background: rgba(0, 100, 200, 0.35) !important;
+  color: #fff !important;
+  font-weight: 600;
+  border-bottom: 1px solid #2b4b8c !important;
+}
+/* 内容单元格 */
+.monitor-container .el-table td.el-table__cell {
+  background: transparent !important;
+  border-bottom: 1px solid rgba(43, 75, 140, 0.25) !important;
+  padding: 7px 0 !important;
+}
+/* 行 */
+.monitor-container .el-table .el-table__row {
+  background: transparent !important;
+}
+.monitor-container .el-table .el-table__row:hover > td.el-table__cell {
+  background: rgba(0, 100, 200, 0.15) !important;
+}
+/* 空数据 */
+.monitor-container .el-table .el-table__empty-block {
+  background: transparent !important;
+}
+.monitor-container .el-table .el-table__empty-text {
+  color: #8090a8;
+}
+/* 滚动条 */
+.monitor-container .el-table .el-scrollbar__bar {
+  opacity: 0.4;
+}
+/* el-select 输入框深色适配 */
+.monitor-container .el-select .el-input__wrapper {
+  background: rgba(30, 40, 90, 0.6);
+  box-shadow: 0 0 0 1px rgba(43, 75, 140, 0.5) inset;
+}
+.monitor-container .el-select .el-input__inner {
+  color: #dce3ee;
+}
+/* el-table 在 flex 容器中撑满高度(无 max-height 时) */
+.monitor-container .el-table {
+  height: 100% !important;
+  display: flex;
+  flex-direction: column;
+}
+.monitor-container .el-table .el-table__inner-wrapper {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+}
+.monitor-container .el-table .el-table__body-wrapper {
+  flex: 1;
+  min-height: 0;
+}
+</style>
+
+<!-- ==================== 页面布局样式(scoped) ==================== -->
 <style scoped>
 .monitor-container {
   display: flex;
   width: 100%;
   height: 100vh;
+  min-height: 600px;
   background: #1a1a4a;
   color: #fff;
   gap: 16px;
   padding: 16px;
   box-sizing: border-box;
+  overflow: hidden;
 }
-
-.left-col, .right-col {
-  width: 24%;
+.left-col {
+  flex: 0 0 25%;
+  min-width: 280px;
+  max-width: 420px;
   display: flex;
   flex-direction: column;
   gap: 16px;
+  min-height: 0;
+}
+.right-col {
+  flex: 0 0 25%;
+  min-width: 280px;
+  max-width: 420px;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  min-height: 0;
 }
-
 .center-col {
   flex: 1;
+  min-width: 0;
   display: flex;
   flex-direction: column;
   gap: 16px;
+  min-height: 0;
 }
-
 .panel {
+  flex: 1;
   background: rgba(30, 40, 90, 0.8);
   border: 1px solid #2b4b8c;
   border-radius: 8px;
-  padding: 12px;
+  padding: 14px;
   box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+}
+/* 设备分类固定高度,不参与均分 */
+.chart-panel {
+  flex: 0 0 190px;
 }
-
 .panel-title {
-  margin: 0 0 12px 0;
-  font-size: 16px;
+  margin: 0 0 10px 0;
+  font-size: 14px;
   color: #66ccff;
   border-left: 3px solid #66ccff;
   padding-left: 8px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  flex-shrink: 0;
+}
+.title-count {
+  font-size: 12px;
+  color: #aaa;
+  font-weight: normal;
 }
-
 .panel-table {
   width: 100%;
-  background: transparent;
-  --el-table-bg-color: transparent;
-  --el-table-row-hover-bg-color: rgba(0, 100, 200, 0.2);
-  --el-table-text-color: #fff;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
 }
-
 .chart {
   width: 100%;
-  height: 180px;
+  flex: 1;
+  min-height: 100px;
 }
-
 .map-panel {
   flex: 1;
   display: flex;
-  align-items: center;
-  justify-content: center;
-  background: #0a0a2a;
+  flex-direction: column;
+  min-height: 0;
 }
-
-.map-placeholder {
+.map-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+  font-size: 14px;
   color: #66ccff;
-  font-size: 18px;
+  flex-shrink: 0;
 }
-
-.priority-tag {
-  padding: 2px 8px;
+.map-legend {
+  display: flex;
+  gap: 12px;
+  font-size: 11px;
+}
+.legend-dot {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  margin-right: 3px;
+}
+.legend-dot.online { background: #67c23a; }
+.legend-dot.alert { background: #f56c6c; }
+.legend-dot.offline { background: #909399; }
+.bmap-container {
+  width: 100%;
+  flex: 1;
+  min-height: 200px;
   border-radius: 4px;
-  font-size: 12px;
+  overflow: hidden;
+}
+.filter-bar {
+  margin-bottom: 10px;
+  flex-shrink: 0;
+}
+.priority-tag {
+  padding: 1px 6px;
+  border-radius: 3px;
+  font-size: 11px;
+  color: #fff;
 }
 .priority-tag.high { background: #ff3333; }
-.priority-tag.medium { background: #ffb333; }
+.priority-tag.medium { background: #ffb333; color: #333; }
 .priority-tag.low { background: #33cc33; }
-
 .status-tag {
-  padding: 2px 8px;
-  border-radius: 4px;
-  font-size: 12px;
+  padding: 1px 6px;
+  border-radius: 3px;
+  font-size: 11px;
+  color: #fff;
 }
 .status-tag.processing { background: #3399ff; }
 .status-tag.done { background: #33cc33; }
-</style>
+.status-dot {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  margin-right: 3px;
+}
+.status-dot.normal { background: #67c23a; }
+.status-dot.alert { background: #f56c6c; }
+.status-dot.offline { background: #909399; }
+.status-text {
+  font-size: 11px;
+  color: #dce3ee;
+}
+
+/* === 响应式 === */
+@media (max-width: 1600px) {
+  .left-col, .right-col { flex: 0 0 24%; min-width: 260px; max-width: 380px; }
+  .monitor-container { gap: 12px; padding: 12px; }
+  .panel { padding: 12px; }
+  .chart-panel { flex-basis: 170px; }
+}
+@media (max-width: 1366px) {
+  .left-col, .right-col { flex: 0 0 27%; min-width: 240px; max-width: 340px; }
+  .monitor-container { gap: 10px; padding: 10px; }
+  .panel { padding: 10px; }
+  .chart-panel { flex-basis: 150px; }
+}
+@media (max-width: 1024px) {
+  .monitor-container {
+    flex-direction: column;
+    height: auto;
+    min-height: 100vh;
+    overflow: auto;
+  }
+  .left-col, .right-col {
+    flex: 1 1 auto;
+    min-width: 100%;
+    max-width: 100%;
+  }
+  .center-col {
+    flex: 1 1 auto;
+    min-width: 100%;
+  }
+}
+</style>

+ 371 - 419
src/views/subSystem/manholeCover/mRealTimemonit/mRStateMonitoring.vue

@@ -1,374 +1,372 @@
-<!--实时监测一张图-->
+<!--窨井盖安全实时监测一张图-->
 <template>
   <div class="gis-monitor-container">
     <!-- 头部面板 -->
     <div class="header-panel">
       <div class="title-logo">
-        <el-icon :size="28"><Monitor /></el-icon>
+        <el-icon :size="26"><Monitor /></el-icon>
         <span class="title">窨井盖安全实时监测一张图</span>
         <span class="version">GIS动态态势平台</span>
       </div>
       <div class="stats-badge">
-        <div class="stat-badge-item">
-          <span class="dot online"></span> 在线 {{ onlineCount }}
-        </div>
-        <div class="stat-badge-item">
-          <span class="dot alert"></span> 告警 {{ alertCount }}
-        </div>
-        <div class="stat-badge-item">
-          <span class="dot offline"></span> 离线 {{ offlineCount }}
-        </div>
-        <div class="stat-badge-item">
-          <el-date-picker v-model="dateRange" type="datetimerange" range-separator="至" size="small" :shortcuts="shortcuts" start-placeholder="开始时间" end-placeholder="结束时间" />
-        </div>
+        <div class="stat-badge-item"><span class="dot online"></span> 在线 {{ stats.online }}</div>
+        <div class="stat-badge-item"><span class="dot alert"></span> 告警 {{ stats.alarm }}</div>
+        <div class="stat-badge-item"><span class="dot offline"></span> 离线 {{ stats.offline }}</div>
+        <div class="stat-badge-item">总计 {{ stats.total }}</div>
+        <el-button size="small" :icon="Refresh" @click="loadData" :loading="loading">刷新</el-button>
       </div>
     </div>
 
-    <!-- 主内容区:左侧图层控制 + 右侧GIS地图 -->
+    <!-- 主内容区:左侧图层控制 + 右侧GIS地图 -->
     <div class="main-layout">
-      <!-- 左侧图层分类控制卡 -->
+      <!-- 左侧图层控制面板 -->
       <div class="layer-panel">
-        <div class="layer-header">
-          <el-icon></el-icon> 动态监测图层
+        <div class="layer-header"><el-icon><Location /></el-icon> 动态监测图层</div>
+        <!-- 类型筛选 -->
+        <div class="layer-section">
+          <div class="layer-section-title">设备类型</div>
+          <el-checkbox-group v-model="typeFilter" @change="refreshMarkers">
+            <el-checkbox label="5">供水窨井</el-checkbox>
+            <el-checkbox label="9">排水窨井</el-checkbox>
+            <el-checkbox label="10">燃气窨井</el-checkbox>
+          </el-checkbox-group>
         </div>
-        <el-tree
-            :data="layerTree"
-            show-checkbox
-            node-key="id"
-            :default-checked-keys="defaultCheckedLayers"
-            @check="onLayerCheck"
-            class="layer-tree"
-        >
-          <template #default="{ node, data }">
-            <span class="custom-tree-node">
-              <el-icon><component :is="data.icon" /></el-icon>
-              <span>{{ node.label }}</span>
-              <el-badge v-if="data.alertCount" :value="data.alertCount" class="badge-alert" type="danger" />
-            </span>
-          </template>
-        </el-tree>
-
-        <!-- 图例说明 -->
+        <!-- 状态筛选 -->
+        <div class="layer-section">
+          <div class="layer-section-title">设备状态</div>
+          <el-checkbox-group v-model="statusFilter" @change="refreshMarkers">
+            <el-checkbox label="normal">正常</el-checkbox>
+            <el-checkbox label="alert">告警</el-checkbox>
+            <el-checkbox label="offline">离线</el-checkbox>
+          </el-checkbox-group>
+        </div>
+        <!-- 图例 -->
         <div class="legend-box">
           <div class="legend-title">图例</div>
-          <div><span class="legend-icon normal-icon"></span> 正常井盖</div>
-          <div><span class="legend-icon warning-icon"></span> 预警井盖</div>
-          <div><span class="legend-icon critical-icon"></span> 严重告警</div>
-          <div><span class="legend-icon water-icon"></span> 井下溢水</div>
-          <div><span class="legend-icon gas-icon"></span> 有害气体</div>
+          <div class="legend-item"><span class="legend-icon normal-icon"></span> 正常设备</div>
+          <div class="legend-item"><span class="legend-icon alert-icon"></span> 告警设备</div>
+          <div class="legend-item"><span class="legend-icon offline-icon"></span> 离线设备</div>
         </div>
       </div>
 
-      <!-- 右侧 GIS 地图核心区域 -->
+      <!-- 右侧 GIS 地图 -->
       <div class="gis-stage">
         <div class="map-toolbar">
-          <el-button-group>
-            <el-button size="small" :icon="ZoomIn" @click="zoomIn">放大</el-button>
-            <el-button size="small" :icon="ZoomOut" @click="zoomOut">缩小</el-button>
-            <el-button size="small" :icon="Location" @click="fitBounds">全图</el-button>
-          </el-button-group>
-          <div class="current-time">实时监测动态更新: {{ currentTime }}</div>
+          <span class="map-title">窨井设备分布图</span>
+          <div class="legend">
+            <span><i class="legend-dot online"></i> 在线</span>
+            <span><i class="legend-dot alert"></i> 告警</span>
+            <span><i class="legend-dot offline"></i> 离线</span>
+          </div>
+        </div>
+        <div class="map-area">
+          <div id="manholeStateMap" class="bmap-container"></div>
         </div>
+      </div>
+    </div>
 
-        <!-- 模拟地图画布 -->
-        <div class="map-wrapper" ref="mapContainer">
-          <canvas id="gisCanvas" ref="canvasRef" @click="handleCanvasClick" @mousemove="onCanvasMouseMove"></canvas>
+    <!-- 设备详情抽屉 -->
+    <el-drawer v-model="drawerVisible" :title="selectedDevice?.name + ' - 监测数据详情'" direction="rtl" size="520px">
+      <div v-if="selectedDevice" v-loading="detailLoading">
+        <!-- 基本信息 -->
+        <el-descriptions :column="1" border size="small">
+          <el-descriptions-item label="设备编号">{{ selectedDevice.code }}</el-descriptions-item>
+          <el-descriptions-item label="设备名称">{{ selectedDevice.name }}</el-descriptions-item>
+          <el-descriptions-item label="设备类型">{{ selectedDevice.typeName }}</el-descriptions-item>
+          <el-descriptions-item label="详细地址">{{ selectedDevice.location }}</el-descriptions-item>
+          <el-descriptions-item label="经纬度" v-if="selectedDevice.lng">{{ selectedDevice.lng }}, {{ selectedDevice.lat }}</el-descriptions-item>
+          <el-descriptions-item label="运行状态">
+            <el-tag :type="getStatusTagType(selectedDevice.status)" size="small">{{ getStatusText(selectedDevice.status) }}</el-tag>
+          </el-descriptions-item>
+        </el-descriptions>
 
-          <!-- 悬浮信息卡片 -->
-          <div v-if="hoveredManhole" class="hover-card" :style="{ top: hoverY + 'px', left: hoverX + 'px' }">
-            <div class="hover-title">{{ hoveredManhole.name }}</div>
-            <div>状态: {{ hoveredManhole.statusText }}</div>
-            <div>位置: {{ hoveredManhole.address }}</div>
-            <div>井下温度: {{ hoveredManhole.env?.temperature }}°C</div>
-            <div>水位: {{ hoveredManhole.env?.waterLevel }}cm</div>
-            <div>气体(H₂S): {{ hoveredManhole.env?.gas }}ppm</div>
-          </div>
-        </div>
+        <!-- 井盖监测数据 (jg_device_data) -->
+        <template v-if="selectedDevice.manholeData">
+          <el-divider content-position="left">井盖监测数据</el-divider>
+          <el-row :gutter="12">
+            <el-col :span="8">
+              <div class="metric-card">
+                <div class="metric-label">倾斜角度</div>
+                <div class="metric-value" :class="{ 'metric-alert': isTiltAlert(selectedDevice.manholeData) }">
+                  {{ selectedDevice.manholeData.tiltAngle || '0' }}°
+                </div>
+                <div class="metric-sub">阈值: {{ selectedDevice.manholeData.angleAlarmThreshold || '-' }}°</div>
+              </div>
+            </el-col>
+            <el-col :span="8">
+              <div class="metric-card">
+                <div class="metric-label">电池电量</div>
+                <div class="metric-value" :class="{ 'metric-warn': Number(selectedDevice.manholeData.batteryLevel) <= 20 }">
+                  {{ selectedDevice.manholeData.batteryLevel || '0' }}%
+                </div>
+              </div>
+            </el-col>
+            <el-col :span="8">
+              <div class="metric-card">
+                <div class="metric-label">信号强度</div>
+                <div class="metric-value" :class="{ 'metric-warn': Number(selectedDevice.manholeData.signalStrength) <= 20 }">
+                  {{ selectedDevice.manholeData.signalStrength || '0' }}%
+                </div>
+              </div>
+            </el-col>
+            <el-col :span="8">
+              <div class="metric-card">
+                <div class="metric-label">温度值</div>
+                <div class="metric-value">{{ selectedDevice.manholeData.temperatureValue || '-' }}°C</div>
+              </div>
+            </el-col>
+            <el-col :span="8">
+              <div class="metric-card">
+                <div class="metric-label">水浸状态</div>
+                <div class="metric-value" :class="{ 'metric-alert': Number(selectedDevice.manholeData.waterInfiltrationAlarmStatus) === 1 }">
+                  {{ Number(selectedDevice.manholeData.waterInfiltrationAlarmStatus) === 1 ? '报警' : '正常' }}
+                </div>
+              </div>
+            </el-col>
+            <el-col :span="8">
+              <div class="metric-card">
+                <div class="metric-label">水位状态</div>
+                <div class="metric-value" :class="{ 'metric-alert': Number(selectedDevice.manholeData.waterLevelAlarmStatus) === 1 }">
+                  {{ Number(selectedDevice.manholeData.waterLevelAlarmStatus) === 1 ? '报警' : '正常' }}
+                </div>
+              </div>
+            </el-col>
+          </el-row>
+          <div class="upload-time">数据上传时间: {{ selectedDevice.manholeData.uploadTime || '-' }}</div>
+        </template>
+        <el-empty v-else description="暂无井盖监测数据" :image-size="60" />
 
-        <!-- 右侧抽屉信息/被监测对象详情标识 -->
-        <el-drawer v-model="drawerVisible" :title="selectedManhole?.name + ' - 监测数据详情'" direction="rtl" size="450px">
-          <div v-if="selectedManhole">
-            <el-descriptions :column="1" border>
-              <el-descriptions-item label="设备ID">{{ selectedManhole.id }}</el-descriptions-item>
-              <el-descriptions-item label="地理位置">{{ selectedManhole.address }}</el-descriptions-item>
-              <el-descriptions-item label="经纬度">{{ selectedManhole.lng }}, {{ selectedManhole.lat }}</el-descriptions-item>
-              <el-descriptions-item label="运行状态">
-                <el-tag :type="getStatusTagType(selectedManhole.status)">{{ selectedManhole.statusText }}</el-tag>
-              </el-descriptions-item>
-              <el-descriptions-item label="井盖状态">
-                <div>倾斜: {{ selectedManhole.statusDetails?.tilt ? '异常' : '正常'}} ({{ selectedManhole.statusDetails?.tiltAngle || 0 }}°)</div>
-                <div>开启: {{ selectedManhole.statusDetails?.open ? '已开启' : '关闭' }}</div>
-                <div>震动: {{ selectedManhole.statusDetails?.vibration ? '剧烈' : '平稳' }}</div>
-              </el-descriptions-item>
-            </el-descriptions>
-            <el-divider>井下物理环境监测</el-divider>
-            <el-row :gutter="12">
-              <el-col :span="12"><el-statistic title="水位深度" :value="selectedManhole.env?.waterLevel || 0" suffix="cm" :value-style="selectedManhole.env?.waterLevel > 30 ? { color: '#f56c6c' } : {}" /></el-col>
-              <el-col :span="12"><el-statistic title="硫化氢(H₂S)" :value="selectedManhole.env?.gas || 0" suffix="ppm" :value-style="selectedManhole.env?.gas > 10 ? { color: '#f56c6c' } : {}" /></el-col>
-              <el-col :span="12"><el-statistic title="温度" :value="selectedManhole.env?.temperature || 0" suffix="°C" /></el-col>
-              <el-col :span="12"><el-statistic title="湿度" :value="selectedManhole.env?.humidity || 0" suffix="%" /></el-col>
-              <el-col :span="12"><el-statistic title="电池电量" :value="selectedManhole.battery || 0" suffix="%" /></el-col>
-              <el-col :span="12"><el-statistic title="信号强度" :value="selectedManhole.signal || 0" suffix="%" /></el-col>
-            </el-row>
-            <el-divider>最新告警记录</el-divider>
-            <el-timeline>
-              <el-timeline-item v-for="alert in selectedManhole.alerts" :key="alert.time" :timestamp="alert.time" :type="alert.type">
-                {{ alert.message }}
-              </el-timeline-item>
-              <el-timeline-item v-if="!selectedManhole.alerts?.length" timestamp="暂无" type="info">设备无告警记录</el-timeline-item>
-            </el-timeline>
+        <!-- 关联井下设备监测数据 -->
+        <el-divider content-position="left">关联井下设备监测</el-divider>
+        <div v-if="relDevices.length === 0" class="empty-rel">暂无关联井下设备</div>
+        <div v-else class="rel-device-list">
+          <div v-for="rel in relDevices" :key="rel.relId" class="rel-device-card">
+            <div class="rel-device-header">
+              <span class="rel-device-name">{{ rel.equipmentName || '未知设备' }}</span>
+              <el-tag size="small" effect="plain">{{ rel.equipmentTypeName || '未知类型' }}</el-tag>
+            </div>
+            <div class="rel-device-location" v-if="rel.equipmentLocation">
+              <el-icon><Location /></el-icon> {{ rel.equipmentLocation }}
+            </div>
+            <!-- 雷达流量计数据 -->
+            <template v-if="rel.monitorData?.dataType === '雷达流量计'">
+              <el-row :gutter="8" class="rel-metrics">
+                <el-col :span="6"><div class="rel-metric"><span class="rm-label">瞬时流量</span><span class="rm-value">{{ rel.monitorData.instantFlow || '-' }}</span></div></el-col>
+                <el-col :span="6"><div class="rel-metric"><span class="rm-label">流速</span><span class="rm-value">{{ rel.monitorData.flowSpeed || '-' }}</span></div></el-col>
+                <el-col :span="6"><div class="rel-metric"><span class="rm-label">水位</span><span class="rm-value">{{ rel.monitorData.waterLevel || '-' }}</span></div></el-col>
+                <el-col :span="6"><div class="rel-metric"><span class="rm-label">累计流量</span><span class="rm-value">{{ rel.monitorData.totalFlow || '-' }}</span></div></el-col>
+              </el-row>
+            </template>
+            <!-- 环境监测数据 -->
+            <template v-else-if="rel.monitorData?.dataType === '环境监测'">
+              <el-row :gutter="8" class="rel-metrics">
+                <el-col :span="8"><div class="rel-metric"><span class="rm-label">温度</span><span class="rm-value">{{ rel.monitorData.temperature ?? '-' }}°C</span></div></el-col>
+                <el-col :span="8"><div class="rel-metric"><span class="rm-label">{{ rel.monitorData.metricName || '湿度' }}</span><span class="rm-value">{{ rel.monitorData.metricValue ?? rel.monitorData.humidity ?? '-' }}</span></div></el-col>
+                <el-col :span="8"><div class="rel-metric"><span class="rm-label">节点</span><span class="rm-value">{{ rel.monitorData.createTime || '-' }}</span></div></el-col>
+              </el-row>
+            </template>
+            <div v-else class="rel-no-data">暂无监测数据</div>
           </div>
-        </el-drawer>
+        </div>
       </div>
-    </div>
+    </el-drawer>
   </div>
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
-import { Monitor, ZoomIn, ZoomOut, Location, Warning, Document, Connection } from '@element-plus/icons-vue'
-
-// --- 图层树结构 (支持分类图层展示)---
-const layerTree = ref([
-  {
-    id: 'layer-status',
-    label: '井盖状态监测图层',
-    icon: 'Warning',
-    alertCount: 2,
-    children: [
-      { id: 'tilt', label: '位移/倾斜监测点', icon: 'Location', alertCount: 1 },
-      { id: 'open', label: '井盖开启监测点', icon: 'Lock', alertCount: 1 },
-      { id: 'vibration', label: '震动异常监测', icon: 'Opportunity', alertCount: 0 }
-    ]
-  },
-  {
-    id: 'layer-environment',
-    label: '井下物理环境监测图层',
-    icon: '',
-    alertCount: 3,
-    children: [
-      { id: 'water', label: '溢水/水位监测', icon: '', alertCount: 2 },
-      { id: 'gas', label: '有害气体监测', icon: 'Warning', alertCount: 1 },
-      { id: 'temp', label: '温湿度监测', icon: 'Temperature', alertCount: 0 }
-    ]
-  },
-  {
-    id: 'layer-device',
-    label: '设备健康度图层',
-    icon: 'Monitor',
-    alertCount: 1,
-    children: [
-      { id: 'battery', label: '电池电压状态', icon: 'Battery', alertCount: 1 },
-      { id: 'signal', label: '通讯信号强度', icon: 'Connection', alertCount: 0 }
-    ]
-  }
-])
-const defaultCheckedLayers = ['layer-status', 'layer-environment', 'tilt', 'open', 'water', 'gas', 'battery']
-const visibleLayers = ref(new Set(defaultCheckedLayers))
-
-const onLayerCheck = (checkedNode, { checkedKeys }) => {
-  visibleLayers.value = new Set(checkedKeys)
-  renderMap()
-}
-
-// --- 模拟窨井盖监测数据 (包含地理位置、监测数据)---
-const manholes = ref([
-  { id: 'M1001', name: '人民路1号井', lng: 121.487, lat: 31.249, address: '人民路与解放路口', status: 'warning', statusText: '倾斜预警', battery: 67, signal: 82,
-    statusDetails: { tilt: true, tiltAngle: 8.5, open: false, vibration: false },
-    env: { waterLevel: 12, gas: 5, temperature: 18, humidity: 65 },
-    alerts: [{ time: '2025-04-02 09:23', message: '井盖倾斜角度超过阈值', type: 'warning' }] },
-  { id: 'M1002', name: '滨江路2号井', lng: 121.502, lat: 31.235, address: '滨江路化工园区', status: 'critical', statusText: '严重告警-溢水+气体超标', battery: 34, signal: 48,
-    statusDetails: { tilt: false, open: false, vibration: true },
-    env: { waterLevel: 58, gas: 28, temperature: 22, humidity: 88 },
-    alerts: [{ time: '2025-04-02 10:15', message: '井下水位超限,有害气体浓度过高', type: 'danger' }] },
-  { id: 'M1003', name: '古城街古井', lng: 121.478, lat: 31.258, address: '古城街北段', status: 'normal', statusText: '正常', battery: 94, signal: 91,
-    statusDetails: { tilt: false, open: false, vibration: false },
-    env: { waterLevel: 5, gas: 1, temperature: 16, humidity: 60 },
-    alerts: [] },
-  { id: 'M1004', name: '高新区智慧井', lng: 121.512, lat: 31.242, address: '高新大道云计算中心', status: 'normal', statusText: '正常', battery: 87, signal: 95,
-    statusDetails: { tilt: false, open: false, vibration: false },
-    env: { waterLevel: 3, gas: 0.5, temperature: 19, humidity: 54 },
-    alerts: [] },
-  { id: 'M1005', name: '南港路车站井', lng: 121.495, lat: 31.228, address: '南港路旧车站', status: 'warning', statusText: '井盖开启告警', battery: 45, signal: 62,
-    statusDetails: { tilt: false, open: true, vibration: false },
-    env: { waterLevel: 9, gas: 3, temperature: 20, humidity: 70 },
-    alerts: [{ time: '2025-04-02 08:45', message: '井盖异常开启', type: 'warning' }] },
-  { id: 'M1006', name: '石化区防爆井', lng: 121.524, lat: 31.239, address: '石化大道', status: 'critical', statusText: '溢水告警', battery: 22, signal: 38,
-    statusDetails: { tilt: false, open: false, vibration: true },
-    env: { waterLevel: 72, gas: 9, temperature: 21, humidity: 91 },
-    alerts: [{ time: '2025-04-02 07:20', message: '水位暴涨,有溢水风险', type: 'danger' }] }
-])
+import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue'
+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'
 
-const onlineCount = computed(() => manholes.value.filter(m => m.signal > 30).length)
-const alertCount = computed(() => manholes.value.filter(m => m.status !== 'normal').length)
-const offlineCount = computed(() => manholes.value.filter(m => m.signal <= 30).length)
+// ==================== 数据状态 ====================
+const loading = ref(false)
+const allDevices = ref([])
+const stats = reactive({ total: 0, online: 0, offline: 0, alarm: 0 })
 
-// GIS画布相关变量
-const canvasRef = ref(null)
-const mapContainer = ref(null)
-let ctx = null
-let mapWidth = 0, mapHeight = 0
-let mapZoom = 1
-let offsetX = 0, offsetY = 0
-let dragStart = null
-let animationFrame = null
+// 筛选条件
+const typeFilter = ref(['5', '9', '10'])
+const statusFilter = ref(['normal', 'alert', 'offline'])
 
-const bounds = { minLng: 121.45, maxLng: 121.55, minLat: 31.22, maxLat: 31.28 }
-
-const dateRange = ref([])
-const shortcuts = [{ text: '最近24小时', value: () => [new Date(Date.now() - 86400000), new Date()] }]
-const currentTime = ref(new Date().toLocaleString())
-
-// 交互状态
-const selectedManhole = ref(null)
-const hoveredManhole = ref(null)
-const hoverX = ref(0), hoverY = ref(0)
+// 选中设备 & 详情
 const drawerVisible = ref(false)
+const detailLoading = ref(false)
+const selectedDevice = ref(null)
+const relDevices = ref([])
 
-setInterval(() => { currentTime.value = new Date().toLocaleString() }, 1000)
+// ==================== 加载数据 ====================
+async function loadData() {
+  loading.value = true
+  try {
+    const res = await getManholeLayerData({ isQueryManholeData: true })
+    const devices = res.data?.devices || []
+    allDevices.value = devices.map(d => {
+      const onlineStatus = Number(d.equipmentStatus?.onlineStatus)
+      const alarmStatus = Number(d.equipmentStatus?.alarmStatus)
+      const manholeAlarm = Number(d.manholeData?.alarmStatus)
+      const isAlert = alarmStatus === 1 || manholeAlarm === 1 ||
+        Number(d.manholeData?.waterInfiltrationAlarmStatus) === 1 ||
+        Number(d.manholeData?.waterLevelAlarmStatus) === 1
+      const isOnline = onlineStatus === 1
+      let status = 'offline'
+      if (isAlert) status = 'alert'
+      else if (isOnline) status = 'normal'
+      return {
+        id: d.equipmentId,
+        code: d.equipmentCode,
+        name: d.equipmentName,
+        typeId: d.equipmentTypeId,
+        typeName: d.equipmentTypeName,
+        location: d.equipmentLocation,
+        lng: d.longitude != null ? Number(d.longitude) : null,
+        lat: d.latitude != null ? Number(d.latitude) : null,
+        status,
+        manholeData: d.manholeData || null
+      }
+    })
+    const statistics = res.data?.statistics || {}
+    stats.total = statistics.total || allDevices.value.length
+    stats.online = statistics.online || 0
+    stats.offline = statistics.offline || 0
+    stats.alarm = statistics.alarm || 0
+    refreshMarkers()
+  } catch (e) {
+    ElMessage.error('设备数据加载失败')
+  } finally {
+    loading.value = false
+  }
+}
 
-function worldToCanvas(lng, lat) {
-  const px = (lng - bounds.minLng) / (bounds.maxLng - bounds.minLng) * mapWidth
-  const py = (1 - (lat - bounds.minLat) / (bounds.maxLat - bounds.minLat)) * mapHeight
-  const centerX = mapWidth / 2, centerY = mapHeight / 2
-  let x = (px - centerX) * mapZoom + centerX + offsetX
-  let y = (py - centerY) * mapZoom + centerY + offsetY
-  return { x, y }
+// ==================== 筛选后的设备 ====================
+function getFilteredDevices() {
+  return allDevices.value.filter(d => {
+    if (!typeFilter.value.includes(d.typeId)) return false
+    if (!statusFilter.value.includes(d.status)) return false
+    return true
+  })
 }
 
-function drawBaseMap() {
-  if (!ctx) return
-  ctx.fillStyle = '#eaf5e9'
-  ctx.fillRect(0, 0, mapWidth, mapHeight)
-  ctx.strokeStyle = '#bfd8bf'
-  ctx.lineWidth = 1
-  for (let i = 0; i < 12; i++) {
-    let x = (i / 12) * mapWidth
-    ctx.beginPath()
-    ctx.moveTo(x, 0)
-    ctx.lineTo(x, mapHeight)
-    ctx.stroke()
-    let y = (i / 12) * mapHeight
-    ctx.beginPath()
-    ctx.moveTo(0, y)
-    ctx.lineTo(mapWidth, y)
-    ctx.stroke()
+// ==================== 百度地图 ====================
+let mapInstance = null
+const markerMap = new Map()
+const YUANLING_CENTER = { lng: 110.393, lat: 28.452 }
+
+function initMap() {
+  const container = document.getElementById('manholeStateMap')
+  if (!container || typeof BMapGL === 'undefined') {
+    console.warn('BMapGL 未加载或容器不存在')
+    return
+  }
+  try {
+    mapInstance = new BMapGL.Map('manholeStateMap')
+    mapInstance.centerAndZoom(new BMapGL.Point(YUANLING_CENTER.lng, YUANLING_CENTER.lat), 13)
+    mapInstance.enableScrollWheelZoom(true)
+  } catch (e) {
+    console.error('百度地图初始化失败:', e)
   }
-  ctx.fillStyle = '#a8d0a6'
-  ctx.font = '12px "Microsoft YaHei"'
-  ctx.fillText('人民路', 200, 120)
-  ctx.fillText('滨江路', 450, 300)
-  ctx.fillText('高新区', 600, 180)
 }
 
-function drawManholes() {
-  if (!ctx) return
-  manholes.value.forEach(mh => {
-    if (mh.signal <= 0) return
-    let { x, y } = worldToCanvas(mh.lng, mh.lat)
-    if (x < -30 || x > mapWidth + 30 || y < -30 || y > mapHeight + 30) return
-    let color = '#67C23A'
-    if (mh.status === 'critical') color = '#F56C6C'
-    else if (mh.status === 'warning') color = '#E6A23C'
-    else color = '#67C23A'
-    ctx.beginPath()
-    ctx.arc(x, y, 14, 0, 2 * Math.PI)
-    ctx.fillStyle = color
-    ctx.fill()
-    ctx.shadowBlur = 0
-    ctx.fillStyle = '#fff'
-    ctx.font = 'bold 14px sans-serif'
-    ctx.fillText(mh.name.slice(0, 3), x-10, y+5)
-    ctx.strokeStyle = '#fff'
-    ctx.lineWidth = 2
-    ctx.stroke()
+function getStatusColor(status) {
+  if (status === 'alert') return '#F56C6C'
+  if (status === 'offline') return '#909399'
+  return '#67C23A'
+}
 
-    // 图层过滤: 动态展示不同监测数据小图标(根据图层可见性绘制周围标注)
-    if (visibleLayers.value.has('water') && mh.env.waterLevel > 30) {
-      ctx.fillStyle = '#3b82f6'
-      ctx.beginPath()
-      ctx.rect(x+12, y-12, 8, 8)
-      ctx.fill()
-    }
-    if (visibleLayers.value.has('gas') && mh.env.gas > 10) {
-      ctx.fillStyle = '#f97316'
-      ctx.beginPath()
-      ctx.rect(x+12, y-4, 8, 8)
-      ctx.fill()
-    }
-    if (visibleLayers.value.has('tilt') && mh.statusDetails.tilt) {
-      ctx.fillStyle = '#ef4444'
-      ctx.beginPath()
-      ctx.moveTo(x+8, y-16)
-      ctx.lineTo(x+16, y-10)
-      ctx.lineTo(x+8, y-4)
-      ctx.fill()
-    }
+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 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}
+    </div>
+    <div><img src="${locationIcon}" style="width:28px;height:28px;display:block;margin:0 auto;"/></div>
+  </div>`
+  const label = new BMapGL.Label(html, {
+    position: bPoint,
+    offset: new BMapGL.Size(-30, -50)
   })
+  label.setStyle({ border: 'none', background: 'transparent', padding: '0', zIndex: '10' })
+  label.addEventListener('click', () => selectDevice(device))
+  mapInstance.addOverlay(label)
+  markerMap.set(device.id, label)
 }
 
-function renderMap() {
-  if (!canvasRef.value) return
-  const canvas = canvasRef.value
-  const container = mapContainer.value
-  if (!container) return
-  mapWidth = container.clientWidth
-  mapHeight = container.clientHeight
-  canvas.width = mapWidth
-  canvas.height = mapHeight
-  ctx = canvas.getContext('2d')
-  drawBaseMap()
-  drawManholes()
+function refreshMarkers() {
+  if (!mapInstance) return
+  markerMap.forEach(label => mapInstance.removeOverlay(label))
+  markerMap.clear()
+  getFilteredDevices().forEach(device => addMapMarker(device))
 }
 
-function zoomIn() { mapZoom = Math.min(mapZoom + 0.1, 2.5); renderMap(); }
-function zoomOut() { mapZoom = Math.max(mapZoom - 0.1, 0.6); renderMap(); }
-function fitBounds() { mapZoom = 1; offsetX = 0; offsetY = 0; renderMap(); }
-
-function handleCanvasClick(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  let minDist = 20, hit = null
-  manholes.value.forEach(mh => {
-    const { x, y } = worldToCanvas(mh.lng, mh.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) { minDist = dist; hit = mh }
-  })
-  if (hit) { selectedManhole.value = hit; drawerVisible.value = true }
+// ==================== 选中设备 & 详情 ====================
+async function selectDevice(device) {
+  selectedDevice.value = device
+  drawerVisible.value = true
+  relDevices.value = []
+  detailLoading.value = true
+  if (mapInstance && device.lng && device.lat) {
+    mapInstance.centerAndZoom(new BMapGL.Point(device.lng, device.lat), 16)
+  }
+  highlightMarker(device.id)
+  try {
+    const res = await getManholeRelDeviceData(device.id)
+    relDevices.value = res.data || []
+  } catch (e) {
+    ElMessage.error('关联设备数据加载失败')
+  } finally {
+    detailLoading.value = false
+  }
 }
 
-function onCanvasMouseMove(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  let minDist = 18, hit = null
-  manholes.value.forEach(mh => {
-    const { x, y } = worldToCanvas(mh.lng, mh.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) { minDist = dist; hit = mh }
+function highlightMarker(id) {
+  markerMap.forEach((label, key) => {
+    const dom = label.getContentContainer ? label.getContentContainer() : null
+    if (dom) {
+      const isSelected = key === id
+      dom.style.zIndex = isSelected ? '999' : '10'
+      const title = dom.querySelector('div')
+      if (title) {
+        title.style.transform = isSelected ? 'scale(1.15)' : 'scale(1)'
+        title.style.transition = 'transform 0.2s'
+        title.style.boxShadow = isSelected ? '0 0 0 2px #409eff' : '0 1px 4px rgba(0,0,0,0.3)'
+      }
+    }
   })
-  if (hit) {
-    hoveredManhole.value = hit
-    hoverX.value = e.clientX + 12
-    hoverY.value = e.clientY - 40
-  } else hoveredManhole.value = null
 }
 
+// ==================== 辅助方法 ====================
+function isTiltAlert(manholeData) {
+  const tilt = Number(manholeData?.tiltAngle)
+  const threshold = Number(manholeData?.angleAlarmThreshold)
+  return threshold > 0 && tilt > threshold
+}
+function getStatusText(status) {
+  return { normal: '正常', alert: '告警', offline: '离线' }[status] || status
+}
 function getStatusTagType(status) {
-  if (status === 'critical') return 'danger'
-  if (status === 'warning') return 'warning'
-  return 'success'
+  return { normal: 'success', alert: 'danger', offline: 'info' }[status] || ''
 }
 
-watch(() => visibleLayers.value, () => renderMap(), { deep: true })
-onMounted(() => {
-  nextTick(() => {
-    renderMap()
-    window.addEventListener('resize', () => renderMap())
-  })
+const handleResize = () => { mapInstance && mapInstance.resize && mapInstance.resize() }
+
+// ==================== 生命周期 ====================
+onMounted(async () => {
+  await nextTick()
+  initMap()
+  window.addEventListener('resize', handleResize)
+  loadData()
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', handleResize)
+  if (mapInstance) {
+    mapInstance.clearOverlays && mapInstance.clearOverlays()
+    mapInstance = null
+  }
+  markerMap.clear()
 })
-onUnmounted(() => window.removeEventListener('resize', () => renderMap()))
 </script>
 
 <style scoped>
@@ -389,128 +387,82 @@ onUnmounted(() => window.removeEventListener('resize', () => renderMap()))
   box-shadow: 0 2px 8px rgba(0,0,0,0.05);
   z-index: 10;
 }
-.title-logo {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
+.title-logo { display: flex; align-items: center; gap: 12px; }
 .title {
-  font-size: 1.4rem;
-  font-weight: 600;
+  font-size: 1.3rem; font-weight: 600;
   background: linear-gradient(135deg, #2e7d32, #1b5e20);
-  -webkit-background-clip: text;
-  background-clip: text;
-  color: transparent;
-}
-.version {
-  background: #e9f5e9;
-  padding: 2px 8px;
-  border-radius: 20px;
-  font-size: 12px;
-}
-.stats-badge {
-  display: flex;
-  gap: 24px;
-  align-items: center;
-}
-.dot {
-  width: 10px;
-  height: 10px;
-  border-radius: 10px;
-  display: inline-block;
-  margin-right: 6px;
+  -webkit-background-clip: text; background-clip: text; color: transparent;
 }
+.version { background: #e9f5e9; padding: 2px 8px; border-radius: 20px; font-size: 12px; }
+.stats-badge { display: flex; gap: 16px; align-items: center; font-size: 13px; }
+.stat-badge-item { display: flex; align-items: center; gap: 4px; }
+.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
 .dot.online { background: #67c23a; }
 .dot.alert { background: #f56c6c; }
 .dot.offline { background: #909399; }
-.main-layout {
-  display: flex;
-  flex: 1;
-  overflow: hidden;
-  gap: 12px;
-  padding: 12px;
-}
+.main-layout { display: flex; flex: 1; overflow: hidden; gap: 12px; padding: 12px; }
 .layer-panel {
-  width: 280px;
-  background: white;
-  border-radius: 20px;
-  padding: 16px;
-  box-shadow: 0 4px 12px rgba(0,0,0,0.05);
-  display: flex;
-  flex-direction: column;
+  width: 240px; background: white; border-radius: 16px; padding: 16px;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.05); display: flex; flex-direction: column;
 }
 .layer-header {
-  font-weight: bold;
-  font-size: 16px;
-  margin-bottom: 16px;
-  display: flex;
-  align-items: center;
-  gap: 8px;
-}
-.layer-tree {
-  flex: 1;
-  overflow-y: auto;
-}
-.custom-tree-node {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-}
-.badge-alert {
-  margin-left: auto;
-}
-.legend-box {
-  margin-top: 24px;
-  border-top: 1px solid #e9ecef;
-  padding-top: 12px;
-}
-.legend-icon {
-  width: 14px;
-  height: 14px;
-  display: inline-block;
-  margin-right: 8px;
-  border-radius: 2px;
+  font-weight: bold; font-size: 15px; margin-bottom: 16px;
+  display: flex; align-items: center; gap: 8px;
 }
+.layer-section { margin-bottom: 16px; }
+.layer-section-title { font-size: 13px; font-weight: 600; color: #606266; margin-bottom: 8px; }
+.legend-box { margin-top: auto; border-top: 1px solid #e9ecef; padding-top: 12px; }
+.legend-title { font-weight: 600; font-size: 13px; margin-bottom: 8px; }
+.legend-item { font-size: 12px; color: #606266; margin-bottom: 6px; display: flex; align-items: center; gap: 6px; }
+.legend-icon { width: 14px; height: 14px; border-radius: 3px; display: inline-block; }
 .normal-icon { background: #67c23a; }
-.warning-icon { background: #e6a23c; }
-.critical-icon { background: #f56c6c; }
-.water-icon { background: #3b82f6; }
-.gas-icon { background: #f97316; }
+.alert-icon { background: #f56c6c; }
+.offline-icon { background: #909399; }
 .gis-stage {
-  flex: 1;
-  background: white;
-  border-radius: 20px;
-  display: flex;
-  flex-direction: column;
-  overflow: hidden;
+  flex: 1; background: white; border-radius: 16px;
+  display: flex; flex-direction: column; overflow: hidden;
   box-shadow: 0 6px 14px rgba(0,0,0,0.08);
 }
 .map-toolbar {
-  padding: 12px 16px;
-  display: flex;
-  justify-content: space-between;
-  border-bottom: 1px solid #f0f0f0;
+  padding: 10px 16px; display: flex; justify-content: space-between;
+  border-bottom: 1px solid #f0f0f0; background: white; z-index: 2;
+}
+.map-title { font-weight: 600; color: #303133; font-size: 14px; }
+.legend { display: flex; gap: 16px; font-size: 12px; align-items: center; }
+.legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; }
+.legend-dot.online { background: #67C23A; }
+.legend-dot.alert { background: #F56C6C; }
+.legend-dot.offline { background: #909399; }
+.map-area { flex: 1; position: relative; min-height: 0; }
+.bmap-container { width: 100%; height: 100%; }
+
+/* 详情抽屉样式 */
+.metric-card {
+  background: #f8f9fc; border-radius: 8px; padding: 10px;
+  text-align: center; margin-bottom: 8px;
 }
-.map-wrapper {
-  flex: 1;
-  position: relative;
-  cursor: crosshair;
+.metric-label { font-size: 12px; color: #909399; margin-bottom: 4px; }
+.metric-value { font-size: 18px; font-weight: bold; color: #303133; }
+.metric-alert { color: #f56c6c; }
+.metric-warn { color: #e6a23c; }
+.metric-sub { font-size: 11px; color: #c0c4cc; margin-top: 2px; }
+.upload-time { font-size: 12px; color: #909399; margin-top: 8px; text-align: right; }
+.empty-rel { text-align: center; color: #909399; padding: 20px; font-size: 13px; }
+.rel-device-list { display: flex; flex-direction: column; gap: 12px; }
+.rel-device-card {
+  background: #f8f9fc; border-radius: 8px; padding: 12px;
+  border: 1px solid #ebeef5;
 }
-canvas {
-  width: 100%;
-  height: 100%;
-  display: block;
+.rel-device-header {
+  display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;
 }
-.hover-card {
-  position: fixed;
-  background: rgba(0,0,0,0.8);
-  backdrop-filter: blur(6px);
-  color: white;
-  border-radius: 12px;
-  padding: 8px 12px;
-  font-size: 12px;
-  pointer-events: none;
-  z-index: 200;
-  white-space: nowrap;
+.rel-device-name { font-weight: 600; font-size: 13px; }
+.rel-device-location { font-size: 12px; color: #606266; display: flex; align-items: center; gap: 4px; margin-bottom: 8px; }
+.rel-metrics { margin-top: 4px; }
+.rel-metric {
+  text-align: center; background: white; border-radius: 6px; padding: 6px;
 }
-</style>
+.rm-label { display: block; font-size: 11px; color: #909399; }
+.rm-value { display: block; font-size: 14px; font-weight: 600; color: #303133; margin-top: 2px; }
+.rel-no-data { font-size: 12px; color: #c0c4cc; text-align: center; padding: 8px; }
+</style>

+ 173 - 96
src/views/subSystem/manholeCover/mRealTimemonit/mRealApicture.vue

@@ -29,9 +29,9 @@
       <div class="device-grid">
         <div class="grid-header">
           <span>监测设备列表</span>
-          <el-input v-model="searchText" placeholder="搜索设备编号/位置" prefix-icon="Search" clearable size="small" style="width: 200px" />
+          <el-input v-model="searchText" placeholder="搜索设备编号/名称" prefix-icon="Search" clearable size="small" style="width: 200px" @keyup.enter="loadDevices" @clear="loadDevices" />
         </div>
-        <div class="card-container">
+        <div class="card-container" v-loading="loading">
           <div
               v-for="device in filteredDevices"
               :key="device.id"
@@ -50,17 +50,17 @@
               <el-icon><Location /></el-icon> {{ device.location }}
             </div>
             <div class="status-icons">
-              <div class="status-item" :class="{ active: device.status.tilt }" title="位移倾斜">
+              <div class="status-item" :class="{ active: device.status.tilt }" title="倾斜告警">
                 <el-icon><TurnOff /></el-icon> 倾斜
               </div>
-              <div class="status-item" :class="{ active: device.status.open }" title="井盖开启">
-                <el-icon><Position /></el-icon> 开启
+              <div class="status-item" :class="{ active: device.status.waterInfiltration }" title="水浸告警">
+                <el-icon><Position /></el-icon> 水浸
               </div>
-              <div class="status-item" :class="{ active: device.status.vibration }" title="异常震动">
-                <el-icon><Opportunity /></el-icon> 震动
+              <div class="status-item" :class="{ active: device.status.waterLevel }" title="水位告警">
+                <el-icon><Opportunity /></el-icon> 水位
               </div>
-              <div class="status-item" :class="{ active: device.status.water }" title="溢水/积水">
-                <el-icon></el-icon> 溢水
+              <div class="status-item" :class="{ active: device.status.lowBattery }" title="低电量">
+                <el-icon><Warning /></el-icon> 低电
               </div>
             </div>
             <div class="device-metrics">
@@ -77,9 +77,11 @@
             </div>
             <div class="card-footer">
               <span class="update-time">最后通讯: {{ device.lastTime }}</span>
-              <el-button v-if="device.hasAlert" type="danger" size="small" plain @click.stop="handleAlert(device)">处理告警</el-button>
+              <el-button v-if="device.hasAlert && !device.workOrderSubmitted" type="danger" size="small" plain @click.stop="handleAlert(device)">处理告警</el-button>
+              <el-tag v-else-if="device.workOrderSubmitted" type="warning" size="small" effect="plain">工单处理中</el-tag>
             </div>
           </div>
+          <el-empty v-if="!loading && filteredDevices.length === 0" description="暂无设备数据" :image-size="80" />
         </div>
       </div>
 
@@ -97,18 +99,18 @@
 
           <!-- 实时状态监测模块 -->
           <el-descriptions :column="2" border class="status-description">
-            <el-descriptions-item label="井盖开启状态">
-              <el-tag :type="selectedDevice.status.open ? 'danger' : 'success'">{{ selectedDevice.status.open ? '已开启' : '关闭' }}</el-tag>
-            </el-descriptions-item>
-            <el-descriptions-item label="位移倾斜状态">
+            <el-descriptions-item label="倾斜状态">
               <el-tag :type="selectedDevice.status.tilt ? 'danger' : 'success'">{{ selectedDevice.status.tilt ? '倾斜告警' : '水平' }}</el-tag>
-              <span v-if="selectedDevice.status.tilt" class="sub-value">(倾斜角: {{ selectedDevice.tiltAngle }}°)</span>
+              <span v-if="selectedDevice.tiltAngle > 0" class="sub-value">(倾斜角: {{ selectedDevice.tiltAngle }}°, 阈值: {{ selectedDevice.threshold }}°)</span>
+            </el-descriptions-item>
+            <el-descriptions-item label="水浸状态">
+              <el-tag :type="selectedDevice.status.waterInfiltration ? 'danger' : 'success'">{{ selectedDevice.status.waterInfiltration ? '水浸告警' : '正常' }}</el-tag>
             </el-descriptions-item>
-            <el-descriptions-item label="异常震动">
-              <el-tag :type="selectedDevice.status.vibration ? 'warning' : 'success'">{{ selectedDevice.status.vibration ? '检测到震动' : '正常' }}</el-tag>
+            <el-descriptions-item label="水位状态">
+              <el-tag :type="selectedDevice.status.waterLevel ? 'danger' : 'success'">{{ selectedDevice.status.waterLevel ? '水位告警' : '正常' }}</el-tag>
             </el-descriptions-item>
-            <el-descriptions-item label="溢水/积水">
-              <el-tag :type="selectedDevice.status.water ? 'warning' : 'success'">{{ selectedDevice.status.water ? '溢水告警' : '干燥' }}</el-tag>
+            <el-descriptions-item label="综合报警">
+              <el-tag :type="selectedDevice.raw?.manholeData?.alarmStatus === '1' ? 'danger' : 'success'">{{ selectedDevice.raw?.manholeData?.alarmStatus === '1' ? '报警中' : '无报警' }}</el-tag>
             </el-descriptions-item>
             <el-descriptions-item label="电池电压">
               <el-progress :percentage="selectedDevice.battery" :format="() => `${selectedDevice.battery}%`" :color="batteryColor(selectedDevice.battery)" />
@@ -131,18 +133,18 @@
             </div>
             <div class="alert-message">
               <ul>
-                <li v-if="selectedDevice.status.open">⚠️ 井盖被异常开启,请立即前往现场检查。</li>
-                <li v-if="selectedDevice.status.tilt">⚠️ 井盖发生位移倾斜({{ selectedDevice.tiltAngle }}°),可能损坏或移位。</li>
-                <li v-if="selectedDevice.status.vibration">⚠️ 持续异常震动,可能存在外力冲击或车辆碾压异常。</li>
-                <li v-if="selectedDevice.status.water">⚠️ 井内水位超限,溢水风险,需排水处理。</li>
-                <li v-if="selectedDevice.battery < 20">🔋 电池电压过低,请及时更换电池。</li>
-                <li v-if="selectedDevice.signal < 30">📶 设备信号弱,可能影响数据传输。</li>
+                <li v-if="selectedDevice.status.tilt">⚠️ 井盖发生位移倾斜({{ selectedDevice.tiltAngle }}°),超过阈值 {{ selectedDevice.threshold }}°,可能损坏或移位。</li>
+                <li v-if="selectedDevice.status.waterInfiltration">⚠️ 检测到水浸报警,井内可能进水,需前往现场检查。</li>
+                <li v-if="selectedDevice.status.waterLevel">⚠️ 水位超限,溢水风险,需排水处理。</li>
+                <li v-if="selectedDevice.status.lowBattery">🔋 电池电压过低({{ selectedDevice.battery }}%),请及时更换电池。</li>
+                <li v-if="selectedDevice.status.lowSignal">📶 设备信号弱({{ selectedDevice.signal }}%),可能影响数据传输。</li>
               </ul>
             </div>
-            <div class="action-buttons">
-              <el-button type="primary" @click="handleDispatch(selectedDevice)">启动处理机制</el-button>
+            <div class="action-buttons" v-if="!selectedDevice.workOrderSubmitted">
+              <el-button type="primary" :loading="submitting" @click="handleDispatch(selectedDevice)">启动处理机制</el-button>
               <el-button @click="simulateFix(selectedDevice)">模拟修复/复位</el-button>
             </div>
+            <el-alert v-else title="工单已派发,正在处理中" description="相关部门将即刻前往现场处理,请耐心等待。" type="success" :closable="false" show-icon class="submitted-alert" />
 
             <!-- 处理记录与工单 -->
             <div class="history-log" v-if="selectedDevice.alertHistory.length">
@@ -198,50 +200,102 @@
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
+import { ref, computed, onMounted } from 'vue'
 import { WarningFilled, Location, TurnOff, Position, Opportunity, Warning, Search } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { getManholeAlarmMonitorList, addManholeWorkOrder } from '@/api/pipeNetwork/basic'
 
-// 模拟设备数据
-const createMockDevices = () => [
-  {
-    id: 'MN-001', name: '人民路智能井盖', location: '人民路与解放路口', battery: 87, voltage: 3.6, signal: 92, signalStrength: '强',
-    status: { open: false, tilt: false, vibration: false, water: false }, hasAlert: false, alertLevel: null, tiltAngle: 0,
-    lastTime: '2025-04-02 10:23', alertHistory: []
-  },
-  {
-    id: 'MN-002', name: '滨江路防爆井盖', location: '滨江路化工园南门', battery: 34, voltage: 2.9, signal: 45, signalStrength: '中',
-    status: { open: true, tilt: true, vibration: false, water: false }, hasAlert: true, alertLevel: 'critical', tiltAngle: 12.5,
-    lastTime: '2025-04-02 09:15', alertHistory: [{ time: '2025-04-01 16:20', content: '触发倾斜告警,系统已记录', type: 'danger' }]
-  },
-  {
-    id: 'MN-003', name: '古城街铸铁井盖', location: '古城街北段', battery: 12, voltage: 2.2, signal: 22, signalStrength: '弱',
-    status: { open: false, tilt: false, vibration: true, water: true }, hasAlert: true, alertLevel: 'critical', tiltAngle: 0,
-    lastTime: '2025-04-02 08:47', alertHistory: [{ time: '2025-04-02 07:30', content: '溢水告警触发', type: 'warning' }]
-  },
-  {
-    id: 'MN-004', name: '高新区智能井盖', location: '高新大道云计算中心', battery: 96, voltage: 3.8, signal: 88, signalStrength: '强',
-    status: { open: false, tilt: false, vibration: false, water: false }, hasAlert: false, alertLevel: null, tiltAngle: 0,
-    lastTime: '2025-04-02 11:02', alertHistory: []
-  },
-  {
-    id: 'MN-005', name: '南港路井盖', location: '南港路旧车站', battery: 55, voltage: 3.2, signal: 67, signalStrength: '中',
-    status: { open: false, tilt: true, vibration: false, water: false }, hasAlert: true, alertLevel: 'warning', tiltAngle: 8.2,
-    lastTime: '2025-04-02 10:05', alertHistory: []
-  }
-]
-
-const devices = ref(createMockDevices())
+/* ============ 状态 ============ */
+const devices = ref([])
+const loading = ref(false)
 const searchText = ref('')
 const selectedDevice = ref(null)
 const dialogVisible = ref(false)
-const taskForm = ref({ taskType: 'repair', owner: '值班人员', remark: '' })
+const submitting = ref(false)
+const taskForm = ref({ taskType: 'repair', owner: '', remark: '' })
+
+/* ============ 数据加载 ============ */
+async function loadDevices() {
+  loading.value = true
+  try {
+    const keyword = searchText.value?.trim()
+    const res = await getManholeAlarmMonitorList({ keyword: keyword || undefined })
+    const result = res.data || {}
+    devices.value = (result.devices || []).map(item => normalizeDevice(item))
+  } catch (error) {
+    devices.value = []
+    const msg = error?.message || error?.msg || '未知错误'
+    console.error('[mRealApicture] 设备数据加载失败:', error)
+    ElMessage.error('设备数据加载失败: ' + msg)
+  } finally {
+    loading.value = false
+  }
+}
+
+/**
+ * 将后端返回的设备数据(含 jg_device_data 最新记录)规范化为前端展示对象
+ *
+ * 报警等级判定逻辑:
+ *   严重告警(critical) — 倾斜角度超阈值 / 水浸报警 / 水位报警
+ *   一般告警(warning)  — 低电量(battery<=20) / 弱信号(signal<=20)
+ */
+function normalizeDevice(raw) {
+  const md = raw?.manholeData || {}
+  const st = raw?.equipmentStatus || {}
 
+  // 数值解析
+  const tiltAngle = parseFloat(md.tiltAngle) || 0
+  const angleThreshold = parseFloat(md.angleAlarmThreshold) || 15
+  const battery = parseFloat(md.batteryLevel) || 0
+  const signal = parseFloat(md.signalStrength) || 0
+  const isOnline = Number(st.onlineStatus) === 1
+
+  // 状态标志
+  const tilt = isOnline && tiltAngle > angleThreshold
+  const waterInfiltration = isOnline && `${md.waterInfiltrationAlarmStatus}` === '1'
+  const waterLevel = isOnline && `${md.waterLevelAlarmStatus}` === '1'
+  const lowBattery = battery > 0 && battery <= 20
+  const lowSignal = signal > 0 && signal <= 20
+
+  // 告警等级:严重 > 一般
+  const isCritical = tilt || waterInfiltration || waterLevel
+  const isWarning = !isCritical && (lowBattery || lowSignal)
+  const hasAlert = isCritical || isWarning
+  const alertLevel = isCritical ? 'critical' : (isWarning ? 'warning' : null)
+
+  // 电压近似值(2.4V ~ 4.2V 映射到 0~100%)
+  const voltage = battery > 0 ? (2.4 + battery * 0.018).toFixed(1) : '—'
+
+  return {
+    id: raw?.equipmentCode || raw?.equipmentId || '',
+    equipmentId: raw?.equipmentId || '',
+    name: raw?.equipmentName || raw?.equipmentCode || '未命名设备',
+    location: raw?.equipmentLocation || '暂无位置信息',
+    battery,
+    voltage,
+    signal,
+    signalStrength: signal > 60 ? '强' : signal > 30 ? '中' : signal > 0 ? '弱' : '—',
+    status: { tilt, waterInfiltration, waterLevel, lowBattery, lowSignal },
+    hasAlert,
+    alertLevel,
+    tiltAngle,
+    threshold: angleThreshold,
+    lastTime: md.uploadTime || md.createTime || '',
+    alertHistory: [],
+    workOrderSubmitted: !!raw?.workOrderSubmitted,
+    raw
+  }
+}
+
+/* ============ 计算属性 ============ */
 const filteredDevices = computed(() => {
   if (!searchText.value) return devices.value
-  return devices.value.filter(d => d.id.toLowerCase().includes(searchText.value.toLowerCase()) || d.name.includes(searchText.value))
+  const kw = searchText.value.toLowerCase()
+  return devices.value.filter(d =>
+    d.id.toLowerCase().includes(kw) || d.name.toLowerCase().includes(kw)
+  )
 })
 
-// 统计告警数据
 const alertStats = computed(() => {
   let critical = 0, warning = 0
   devices.value.forEach(d => {
@@ -254,80 +308,105 @@ const alertStats = computed(() => {
 })
 
 const onlineRate = computed(() => {
-  // 模拟在线判断: 最后通讯时间在一小时内的视为在线,简单起见所有设备在线率基于信号>0
+  if (!devices.value.length) return 0
   const online = devices.value.filter(d => d.signal > 0).length
   return Math.round((online / devices.value.length) * 100)
 })
+
 const avgBattery = computed(() => {
+  if (!devices.value.length) return 0
   const sum = devices.value.reduce((acc, d) => acc + d.battery, 0)
   return Math.round(sum / devices.value.length)
 })
-const hasActiveAlert = computed(() => alertStats.value.critical > 0 || alertStats.value.warning > 0)
 
+/* ============ 辅助函数 ============ */
 const batteryColor = (percent) => percent < 20 ? '#f56c6c' : percent < 50 ? '#e6a23c' : '#67c23a'
 const signalColor = (percent) => percent < 30 ? '#f56c6c' : percent < 60 ? '#e6a23c' : '#67c23a'
 
+/* ============ 交互操作 ============ */
 const selectDevice = (device) => {
   selectedDevice.value = device
 }
 
 const handleAlert = (device) => {
   selectedDevice.value = device
-  // 滚动到右侧面板
 }
 
-const handleDispatch = (device) => {
+const handleDispatch = () => {
+  taskForm.value = { taskType: 'repair', owner: '', remark: '' }
   dialogVisible.value = true
 }
 
-const submitTask = () => {
-  if (selectedDevice.value) {
-    const newLog = {
+// 工单类型映射: taskType → orderType(1-故障维修 2-日常巡检 3-设备保养)
+const ORDER_TYPE_MAP = { repair: 1, inspect: 2, drain: 1, replace: 3 }
+const TYPE_LABEL_MAP = { repair: '故障维修', inspect: '日常巡检', drain: '排水作业', replace: '设备保养' }
+
+const submitTask = async () => {
+  if (!selectedDevice.value?.equipmentId) {
+    ElMessage.warning('设备信息缺失,无法提交工单')
+    return
+  }
+  if (!taskForm.value.remark?.trim()) {
+    ElMessage.warning('请填写工单问题描述')
+    return
+  }
+  submitting.value = true
+  try {
+    const device = selectedDevice.value
+    await addManholeWorkOrder({
+      deviceId: device.equipmentId,
+      orderType: ORDER_TYPE_MAP[taskForm.value.taskType] || 1,
+      orderLevel: device.alertLevel === 'critical' ? 1 : 2,
+      orderDesc: `[${TYPE_LABEL_MAP[taskForm.value.taskType] || '故障维修'}] ${taskForm.value.remark}${taskForm.value.owner ? '(负责人:' + taskForm.value.owner + ')' : ''}`
+    })
+    // 提交成功:标记工单已派发,隐藏按钮
+    device.workOrderSubmitted = true
+    const typeLabel = TYPE_LABEL_MAP[taskForm.value.taskType] || '处理'
+    device.alertHistory.unshift({
       time: new Date().toLocaleString(),
-      content: `已派发${taskForm.value.taskType === 'repair' ? '维修' : taskForm.value.taskType === 'inspect' ? '巡检' : taskForm.value.taskType === 'drain' ? '排水作业' : '电池更换'}工单,负责人:${taskForm.value.owner},备注:${taskForm.value.remark || '无'}`,
+      content: `已派发${typeLabel}工单${taskForm.value.owner ? ',负责人:' + taskForm.value.owner : ''},${taskForm.value.remark}`,
       type: 'primary'
-    }
-    if (!selectedDevice.value.alertHistory) selectedDevice.value.alertHistory = []
-    selectedDevice.value.alertHistory.unshift(newLog)
-
-    // 模拟处理后自动清除告警(如果在处置中置为修复,为了方便展示,不清除演示,点模拟修复会清除)
-    ElMessage.success('处置工单已派发,相关部门将即刻处理')
+    })
+    ElMessage.success('工单已派发,相关部门将即刻处理')
     dialogVisible.value = false
-    taskForm.value = { taskType: 'repair', owner: '值班人员', remark: '' }
+  } catch (error) {
+    const msg = error?.message || error?.msg || '未知错误'
+    ElMessage.error('工单派发失败: ' + msg)
+  } finally {
+    submitting.value = false
   }
 }
 
 const simulateFix = (device) => {
-  // 模拟修复:关闭所有异常标志,清除告警,重置电池略回升
-  device.status = { open: false, tilt: false, vibration: false, water: false }
+  device.status = { tilt: false, waterInfiltration: false, waterLevel: false, lowBattery: false, lowSignal: false }
   device.hasAlert = false
   device.alertLevel = null
   device.tiltAngle = 0
   if (device.battery < 30) device.battery += 20
   const fixLog = { time: new Date().toLocaleString(), content: '运维人员已现场检修,设备恢复正常', type: 'success' }
-  if (!device.alertHistory) device.alertHistory = []
   device.alertHistory.unshift(fixLog)
   ElMessage.success('已模拟修复,设备状态恢复正常')
 }
 
 const simulateAlert = (device) => {
-  // 模拟告警(测试用)
   device.status.tilt = true
   device.tiltAngle = 9.3
-  device.status.open = true
+  device.status.waterLevel = true
   device.hasAlert = true
   device.alertLevel = 'critical'
-  const alertLog = { time: new Date().toLocaleString(), content: '【模拟触发】井盖开启+倾斜告警', type: 'danger' }
+  const alertLog = { time: new Date().toLocaleString(), content: '【模拟触发】倾斜+水位告警', type: 'danger' }
   device.alertHistory.unshift(alertLog)
   ElMessage.warning('已触发模拟告警,请及时处置')
 }
 
 const refreshDevice = () => {
-  // 模拟刷新数据
-  ElMessage.info('已刷新设备状态')
+  loadDevices()
 }
 
-import { ElMessage } from 'element-plus'
+/* ============ 生命周期 ============ */
+onMounted(() => {
+  loadDevices()
+})
 </script>
 
 <style scoped>
@@ -343,16 +422,6 @@ import { ElMessage } from 'element-plus'
   margin-bottom: 20px;
   box-shadow: 0 2px 8px rgba(0,0,0,0.04);
 }
-.title-area {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  margin-bottom: 16px;
-}
-.title-area h2 {
-  margin: 0;
-  font-weight: 600;
-}
 .stats-row {
   display: flex;
   gap: 24px;
@@ -499,6 +568,11 @@ import { ElMessage } from 'element-plus'
 .status-description {
   margin-top: 10px;
 }
+.sub-value {
+  margin-left: 8px;
+  font-size: 12px;
+  color: #909399;
+}
 .alert-section {
   margin-top: 24px;
   border: 1px solid #ffd6d6;
@@ -534,10 +608,13 @@ import { ElMessage } from 'element-plus'
 .normal-message {
   margin-top: 40px;
 }
+.submitted-alert {
+  margin: 16px 0;
+}
 .empty-panel {
   height: 100%;
   display: flex;
   align-items: center;
   justify-content: center;
 }
-</style>
+</style>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 425 - 396
src/views/subSystem/manholeCover/operation/faultMana.vue


+ 389 - 187
src/views/subSystem/manholeCover/operation/mOdataStatistics.vue

@@ -1,4 +1,4 @@
-<!--数据统计-->
+<!--数据统计 - 井盖设备智能终端运维看板-->
 <template>
   <div class="terminal-operation-dashboard">
     <!-- 页面头部 -->
@@ -6,51 +6,51 @@
       <div class="header-left">
         <el-icon><Monitor /></el-icon>
         <span class="title">井盖设备智能终端运维看板</span>
-        <el-tag type="danger" effect="dark">异常终端 {{ abnormalCount }}</el-tag>
+        <el-tag type="danger" effect="dark">异常终端 {{ kpi.abnormal }}</el-tag>
       </div>
       <div class="header-right">
         <el-date-picker
-            v-model="dateRange"
-            type="daterange"
-            range-separator="至"
-            start-placeholder="开始日期"
-            end-placeholder="结束日期"
-            style="width: 260px"
-            size="default"
+          v-model="dateRange"
+          type="daterange"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          style="width: 260px"
+          size="default"
+          @change="loadAll"
         />
-        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
-        <el-button :icon="Download" @click="exportReport">导出报表</el-button>
+        <el-button type="primary" :icon="Refresh" @click="loadAll" :loading="loading">刷新</el-button>
       </div>
     </div>
 
     <!-- KPI 指标卡片 -->
     <div class="kpi-cards">
       <div class="kpi-card">
-        <div class="kpi-value">{{ totalDevices }}</div>
+        <div class="kpi-value">{{ kpi.total }}</div>
         <div class="kpi-label">终端总数</div>
       </div>
       <div class="kpi-card warning">
-        <div class="kpi-value">{{ abnormalCount }}</div>
+        <div class="kpi-value">{{ kpi.abnormal }}</div>
         <div class="kpi-label">异常终端</div>
-        <div class="kpi-trend">占比 {{ abnormalRate }}%</div>
+        <div class="kpi-trend">占比 {{ kpi.abnormalRate }}%</div>
       </div>
       <div class="kpi-card success">
-        <div class="kpi-value">{{ problemSolvedRate }}%</div>
+        <div class="kpi-value">{{ kpi.solvedRate }}%</div>
         <div class="kpi-label">问题解决率</div>
-        <div class="kpi-trend">环比 +2.5%</div>
+        <div class="kpi-trend">已派发 {{ kpi.submittedCount }} / 异常 {{ kpi.abnormal }}</div>
       </div>
       <div class="kpi-card">
-        <div class="kpi-value">{{ caseDisposalRate }}%</div>
+        <div class="kpi-value">{{ kpi.disposalRate }}%</div>
         <div class="kpi-label">案件处置率</div>
-        <div class="kpi-trend">同比 +5.2%</div>
+        <div class="kpi-trend">工单总数 {{ kpi.totalCase }}</div>
       </div>
       <div class="kpi-card">
-        <div class="kpi-value">{{ delayRate }}%</div>
+        <div class="kpi-value">{{ kpi.delayRate }}%</div>
         <div class="kpi-label">延期率</div>
-        <div class="kpi-trend trend-down">环比 -1.2%</div>
+        <div class="kpi-trend" :class="{ 'trend-down': Number(kpi.delayRate) > 0 }">统计范围内</div>
       </div>
       <div class="kpi-card">
-        <div class="kpi-value">{{ avgResponseTime }}</div>
+        <div class="kpi-value">{{ kpi.avgResponseTime }}</div>
         <div class="kpi-label">平均响应时长(小时)</div>
       </div>
     </div>
@@ -59,23 +59,15 @@
     <div class="charts-row">
       <div class="chart-card">
         <div class="chart-header">
-          <span>问题数量趋势(近6个月)</span>
-          <el-radio-group v-model="trendPeriod" size="small">
-            <el-radio-button label="month">月度</el-radio-button>
-            <el-radio-button label="week">周度</el-radio-button>
-          </el-radio-group>
-        </div>
-        <div class="chart-container">
-          <v-chart :option="trendChartOption" autoresize />
+          <span>异常终端数量趋势(月度)</span>
         </div>
+        <div ref="trendChartRef" class="chart-container" v-loading="loading"></div>
       </div>
       <div class="chart-card">
         <div class="chart-header">
           <span>问题类型分布</span>
         </div>
-        <div class="chart-container">
-          <v-chart :option="problemTypeOption" autoresize />
-        </div>
+        <div ref="problemTypeChartRef" class="chart-container" v-loading="loading"></div>
       </div>
     </div>
 
@@ -84,41 +76,38 @@
         <div class="chart-header">
           <span>处置率 vs 延期率趋势</span>
         </div>
-        <div class="chart-container">
-          <v-chart :option="disposalTrendOption" autoresize />
-        </div>
+        <div ref="disposalTrendChartRef" class="chart-container" v-loading="loading"></div>
       </div>
       <div class="chart-card">
         <div class="chart-header">
           <span>区域运维情况排行</span>
         </div>
-        <div class="chart-container">
-          <v-chart :option="regionRankOption" autoresize />
-        </div>
+        <div ref="regionRankChartRef" class="chart-container" v-loading="loading"></div>
       </div>
     </div>
 
     <!-- 异常终端列表 -->
     <div class="device-list-container">
       <div class="list-header">
-        <span><el-icon><List /></el-icon> 异常终端列表(默认展示异常终端)</span>
+        <span><el-icon><List /></el-icon> 异常终端列表(共 {{ filteredAbnormalDevices.length }} 台)</span>
         <div class="list-filters">
           <el-input
-              v-model="searchKeyword"
-              placeholder="搜索设备编号/名称/位置"
-              clearable
-              :prefix-icon="Search"
-              style="width: 200px"
-              size="small"
+            v-model="searchKeyword"
+            placeholder="搜索设备编号/名称/位置"
+            clearable
+            :prefix-icon="Search"
+            style="width: 200px"
+            size="small"
           />
-          <el-select v-model="faultTypeFilter" placeholder="故障类型" clearable size="small" style="width: 120px">
+          <el-select v-model="faultTypeFilter" placeholder="故障类型" clearable size="small" style="width: 130px">
             <el-option label="倾斜超标" value="tilt" />
-            <el-option label="水位超限" value="water" />
-            <el-option label="异常震动" value="vibration" />
-            <el-option label="通讯故障" value="communication" />
+            <el-option label="水位超限" value="waterLevel" />
+            <el-option label="水浸报警" value="waterInfiltration" />
+            <el-option label="通讯故障" value="offline" />
             <el-option label="电池低电量" value="battery" />
+            <el-option label="信号弱" value="signal" />
           </el-select>
-          <el-button type="primary" size="small" @click="refreshList">查询</el-button>
+          <el-button type="primary" size="small" @click="currentPage = 1">查询</el-button>
         </div>
       </div>
       <el-table :data="paginatedAbnormalDevices" stripe border style="width: 100%" v-loading="loading">
@@ -126,17 +115,19 @@
         <el-table-column prop="code" label="设备编号" width="140" sortable />
         <el-table-column prop="name" label="设备名称" width="140" show-overflow-tooltip />
         <el-table-column prop="deviceTypeName" label="设备类型" width="100" />
-        <el-table-column prop="faultTypeName" label="故障类型" width="110">
+        <el-table-column label="故障类型" width="110">
           <template #default="{ row }">
-            <el-tag :type="getFaultTagType(row.faultType)" size="small">{{ row.faultTypeName }}</el-tag>
+            <el-tag v-for="ft in row.faultTypes" :key="ft.type" :type="getFaultTagType(ft.type)" size="small" style="margin: 1px">{{ ft.name }}</el-tag>
           </template>
         </el-table-column>
         <el-table-column prop="faultDesc" label="故障描述" min-width="180" show-overflow-tooltip />
         <el-table-column prop="locationShort" label="所在位置" min-width="140" show-overflow-tooltip />
-        <el-table-column prop="faultTime" label="故障时间" width="150" sortable />
-        <el-table-column prop="status" label="处理状态" width="100" align="center">
+        <el-table-column prop="abnormalTime" label="故障时间" width="150" sortable />
+        <el-table-column label="处理状态" width="100" align="center">
           <template #default="{ row }">
-            <el-tag :type="getStatusTag(row.status)" size="small">{{ getStatusText(row.status) }}</el-tag>
+            <el-tag :type="row.workOrderSubmitted ? 'success' : 'danger'" size="small">
+              {{ row.workOrderSubmitted ? '已派发' : '待派发' }}
+            </el-tag>
           </template>
         </el-table-column>
         <el-table-column prop="elapsedHours" label="已耗时(h)" width="100" sortable align="center">
@@ -152,12 +143,12 @@
       </el-table>
       <div class="pagination-wrapper">
         <el-pagination
-            background
-            layout="total, sizes, prev, pager, next"
-            :total="filteredAbnormalDevices.length"
-            v-model:current-page="currentPage"
-            v-model:page-size="pageSize"
-            :page-sizes="[10, 20, 50]"
+          background
+          layout="total, sizes, prev, pager, next"
+          :total="filteredAbnormalDevices.length"
+          v-model:current-page="currentPage"
+          v-model:page-size="pageSize"
+          :page-sizes="[10, 20, 50]"
         />
       </div>
     </div>
@@ -169,25 +160,44 @@
           <el-descriptions-item label="设备编号">{{ currentDevice.code }}</el-descriptions-item>
           <el-descriptions-item label="设备名称">{{ currentDevice.name }}</el-descriptions-item>
           <el-descriptions-item label="设备类型">{{ currentDevice.deviceTypeName }}</el-descriptions-item>
-          <el-descriptions-item label="设备型号">{{ currentDevice.model }}</el-descriptions-item>
-          <el-descriptions-item label="权属单位">{{ currentDevice.ownerUnit }}</el-descriptions-item>
-          <el-descriptions-item label="运维部门">{{ currentDevice.maintenanceDept }}</el-descriptions-item>
-          <el-descriptions-item label="故障类型">{{ currentDevice.faultTypeName }}</el-descriptions-item>
           <el-descriptions-item label="故障描述">{{ currentDevice.faultDesc }}</el-descriptions-item>
-          <el-descriptions-item label="故障时间">{{ currentDevice.faultTime }}</el-descriptions-item>
+          <el-descriptions-item label="故障时间">{{ currentDevice.abnormalTime }}</el-descriptions-item>
+          <el-descriptions-item label="详细地址">{{ currentDevice.location }}</el-descriptions-item>
           <el-descriptions-item label="处理状态">
-            <el-tag :type="getStatusTag(currentDevice.status)">{{ getStatusText(currentDevice.status) }}</el-tag>
+            <el-tag :type="currentDevice.workOrderSubmitted ? 'success' : 'danger'">
+              {{ currentDevice.workOrderSubmitted ? '已派发' : '待派发' }}
+            </el-tag>
           </el-descriptions-item>
-          <el-descriptions-item label="已耗时">{{ currentDevice.elapsedHours }} 小时</el-descriptions-item>
-          <el-descriptions-item label="详细地址">{{ currentDevice.address }}</el-descriptions-item>
         </el-descriptions>
-        <div class="history-section" v-if="currentDevice.historyRecords?.length">
-          <div class="sub-title">处置记录</div>
-          <el-timeline>
-            <el-timeline-item v-for="record in currentDevice.historyRecords" :key="record.id" :timestamp="record.time" :type="record.type">
-              {{ record.content }}
-            </el-timeline-item>
-          </el-timeline>
+
+        <!-- 监测数据 -->
+        <div class="sub-title">监测数据</div>
+        <div class="monitor-grid">
+          <div class="monitor-item" :class="{ alert: currentDevice.status.tilt }">
+            <div class="m-label">倾斜角度</div>
+            <div class="m-value">{{ currentDevice.tiltAngle }}°</div>
+            <div class="m-extra">阈值: {{ currentDevice.angleThreshold }}°</div>
+          </div>
+          <div class="monitor-item" :class="{ alert: currentDevice.status.waterInfiltration }">
+            <div class="m-label">水浸</div>
+            <div class="m-value">{{ currentDevice.status.waterInfiltration ? '报警' : '正常' }}</div>
+          </div>
+          <div class="monitor-item" :class="{ alert: currentDevice.status.waterLevel }">
+            <div class="m-label">水位</div>
+            <div class="m-value">{{ currentDevice.status.waterLevel ? '超标' : '正常' }}</div>
+          </div>
+          <div class="monitor-item" :class="{ alert: currentDevice.status.lowBattery }">
+            <div class="m-label">电量</div>
+            <div class="m-value">{{ currentDevice.battery }}%</div>
+          </div>
+          <div class="monitor-item" :class="{ alert: currentDevice.status.lowSignal }">
+            <div class="m-label">信号</div>
+            <div class="m-value">{{ currentDevice.signal }}%</div>
+          </div>
+          <div class="monitor-item">
+            <div class="m-label">温度</div>
+            <div class="m-value">{{ currentDevice.temperature || '—' }}°C</div>
+          </div>
         </div>
       </div>
     </el-drawer>
@@ -195,145 +205,311 @@
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
-import { Monitor, Refresh, Download, List, Search } from '@element-plus/icons-vue'
-import { use } from 'echarts/core'
-import { CanvasRenderer } from 'echarts/renderers'
-import { BarChart, LineChart, PieChart } from 'echarts/charts'
-import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, ToolboxComponent } from 'echarts/components'
-
-use([CanvasRenderer, BarChart, LineChart, PieChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, ToolboxComponent])
-
-// 模拟设备数据(含异常终端)
-const mockDevices = ref([
-  { id: 1, code: 'DEV-1001', name: '人民路智能井盖', deviceType: 'smart', deviceTypeName: '智能井盖', model: 'WATCHMAN-S2', ownerUnit: '市政工程管理处', maintenanceDept: '市政维修一队', faultType: 'tilt', faultTypeName: '倾斜超标', faultDesc: '倾斜角度14.2°,超过阈值8°', faultTime: '2025-04-02 14:23:15', status: 'pending', statusText: '待处理', elapsedHours: 2.5, lng: 121.487, lat: 31.249, address: '人民路与解放路口东50m', locationShort: '人民路口', historyRecords: [] },
-  { id: 2, code: 'DEV-1002', name: '滨江路防爆井盖', deviceType: 'explosion', deviceTypeName: '防爆井盖', model: 'WATCHMAN-EX', ownerUnit: '水务集团', maintenanceDept: '水务抢修队', faultType: 'water', faultTypeName: '水位超限', faultDesc: '水位62cm,超过阈值40cm', faultTime: '2025-04-02 13:55:02', status: 'pending', statusText: '待处理', elapsedHours: 3.2, lng: 121.502, lat: 31.235, address: '滨江路化工园区南门', locationShort: '滨江园区', historyRecords: [] },
-  { id: 3, code: 'DEV-1003', name: '古城街井盖', deviceType: 'smart', deviceTypeName: '智能井盖', model: 'WATCHMAN-S1', ownerUnit: '市政设施管理处', maintenanceDept: '市政维修二队', faultType: 'vibration', faultTypeName: '异常震动', faultDesc: '震动112mg,超过阈值80mg', faultTime: '2025-04-02 12:10:33', status: 'processing', statusText: '处理中', elapsedHours: 5.8, lng: 121.478, lat: 31.258, address: '古城街北段树下', locationShort: '古城街', historyRecords: [{ id: 1, time: '2025-04-02 13:00:00', type: 'primary', content: '维修人员已出发前往现场' }] },
-  { id: 4, code: 'DEV-1004', name: '南港路井盖', deviceType: 'smart', deviceTypeName: '智能井盖', model: 'WATCHMAN-S2', ownerUnit: '市政工程管理处', maintenanceDept: '市政维修一队', faultType: 'communication', faultTypeName: '通讯故障', faultDesc: '设备离线超2小时', faultTime: '2025-04-02 11:45:20', status: 'pending', statusText: '待处理', elapsedHours: 6.3, lng: 121.495, lat: 31.228, address: '南港路旧车站段', locationShort: '南港路', historyRecords: [] },
-  { id: 5, code: 'DEV-1005', name: '石化区防爆井盖', deviceType: 'explosion', deviceTypeName: '防爆井盖', model: 'WATCHMAN-EX', ownerUnit: '生态环境局', maintenanceDept: '应急抢修队', faultType: 'battery', faultTypeName: '电池低电量', faultDesc: '电池电量仅剩12%', faultTime: '2025-04-02 10:30:45', status: 'completed', statusText: '已完成', elapsedHours: 4.5, lng: 121.524, lat: 31.239, address: '石化大道南侧', locationShort: '石化大道', historyRecords: [{ id: 1, time: '2025-04-02 11:30:00', type: 'success', content: '电池更换完成' }] },
-  { id: 6, code: 'DEV-1006', name: '高新区智能井盖', deviceType: 'smart', deviceTypeName: '智能井盖', model: 'WATCHMAN-S2', ownerUnit: '市政设施管理处', maintenanceDept: '市政维修二队', faultType: 'tilt', faultTypeName: '倾斜超标', faultDesc: '倾斜角度5.6°', faultTime: '2025-04-02 09:20:00', status: 'pending', statusText: '待处理', elapsedHours: 8.7, lng: 121.512, lat: 31.242, address: '高新大道云计算中心旁', locationShort: '高新区', historyRecords: [] },
-  { id: 7, code: 'DEV-1007', name: '中山路智能井盖', deviceType: 'smart', deviceTypeName: '智能井盖', model: 'WATCHMAN-S2', ownerUnit: '市政工程管理处', maintenanceDept: '市政维修一队', faultType: 'water', faultTypeName: '水位超限', faultDesc: '水位45cm', faultTime: '2025-04-01 16:30:00', status: 'completed', statusText: '已完成', elapsedHours: 12, lng: 121.488, lat: 31.252, address: '中山路与人民路口', locationShort: '中山路口', historyRecords: [] },
-  { id: 8, code: 'DEV-1008', name: '迎宾大道井盖', deviceType: 'smart', deviceTypeName: '智能井盖', model: 'WATCHMAN-S1', ownerUnit: '市政设施管理处', maintenanceDept: '市政维修二队', faultType: 'vibration', faultTypeName: '异常震动', faultDesc: '震动95mg', faultTime: '2025-04-01 14:00:00', status: 'completed', statusText: '已完成', elapsedHours: 8, lng: 121.505, lat: 31.245, address: '迎宾大道88号', locationShort: '迎宾大道', historyRecords: [] }
-])
-
-// 正常设备数量(模拟)
-const normalDeviceCount = 42
-const totalDevices = computed(() => mockDevices.value.length + normalDeviceCount)
-const abnormalDevices = computed(() => mockDevices.value.filter(d => d.status !== 'completed'))
-const abnormalCount = computed(() => abnormalDevices.value.length)
-const abnormalRate = computed(() => ((abnormalCount.value / totalDevices.value) * 100).toFixed(1))
-
-// 运维指标模拟
-const problemSolvedRate = computed(() => {
-  const completed = mockDevices.value.filter(d => d.status === 'completed').length
-  const total = mockDevices.value.length
-  return Math.round((completed / total) * 100)
-})
-const caseDisposalRate = computed(() => 94)
-const delayRate = computed(() => 8.5)
-const avgResponseTime = computed(() => 3.2)
+import { ref, reactive, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
+import { Monitor, Refresh, List, Search } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import * as echarts from 'echarts'
+import {
+  getManholeMaintenanceDashboard
+} from '@/api/pipeNetwork/basic'
+
+// ==================== 常量 ====================
+const TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
+const LOW_BATTERY_THRESHOLD = 15
+const LOW_SIGNAL_THRESHOLD = 20
 
-// 筛选条件
+// ==================== 状态 ====================
+const loading = ref(false)
+const dateRange = ref([])
 const searchKeyword = ref('')
 const faultTypeFilter = ref('')
-const dateRange = ref([])
-const trendPeriod = ref('month')
 const currentPage = ref(1)
 const pageSize = ref(10)
-const loading = ref(false)
+
 const detailDrawerVisible = ref(false)
 const currentDevice = ref(null)
 
-// 过滤异常终端
+const kpi = reactive({
+  total: 0, abnormal: 0, abnormalRate: '0.0',
+  solvedRate: 0, submittedCount: 0, totalCase: 0,
+  disposalRate: 0, delayRate: 0,
+  avgResponseTime: '—'
+})
+
+const abnormalDevices = ref([])
+
+// 图表引用
+const trendChartRef = ref(null)
+const problemTypeChartRef = ref(null)
+const disposalTrendChartRef = ref(null)
+const regionRankChartRef = ref(null)
+let trendChart = null, problemTypeChart = null, disposalTrendChart = null, regionRankChart = null
+
+// ==================== 日期工具 ====================
+function formatDate(date) {
+  const y = date.getFullYear()
+  const m = String(date.getMonth() + 1).padStart(2, '0')
+  const d = String(date.getDate()).padStart(2, '0')
+  return `${y}-${m}-${d}`
+}
+
+// ==================== 设备数据规范化 ====================
+function normalizeDevice(raw) {
+  const md = raw?.manholeData || {}
+  const st = raw?.equipmentStatus || {}
+  const typeId = raw?.equipmentTypeId || ''
+
+  const tiltAngle = parseFloat(md.tiltAngle) || 0
+  const angleThreshold = parseFloat(md.angleAlarmThreshold) || 15
+  const battery = parseFloat(md.batteryLevel) || 0
+  const signal = parseFloat(md.signalStrength) || 0
+  const temperature = md.temperatureValue ? parseFloat(md.temperatureValue).toFixed(1) : null
+  const isOnline = Number(st.onlineStatus) === 1
+
+  const tilt = isOnline && tiltAngle > angleThreshold
+  const waterInfiltration = isOnline && `${md.waterInfiltrationAlarmStatus}` === '1'
+  const waterLevel = isOnline && `${md.waterLevelAlarmStatus}` === '1'
+  const lowBattery = battery > 0 && battery <= LOW_BATTERY_THRESHOLD
+  const lowSignal = signal > 0 && signal <= LOW_SIGNAL_THRESHOLD
+  const offline = !isOnline
+
+  const faultTypes = []
+  if (tilt) faultTypes.push({ type: 'tilt', name: '倾斜超标' })
+  if (waterLevel) faultTypes.push({ type: 'waterLevel', name: '水位超限' })
+  if (waterInfiltration) faultTypes.push({ type: 'waterInfiltration', name: '水浸报警' })
+  if (offline) faultTypes.push({ type: 'offline', name: '通讯故障' })
+  if (lowBattery) faultTypes.push({ type: 'battery', name: '电池低电量' })
+  if (lowSignal) faultTypes.push({ type: 'signal', name: '信号弱' })
+
+  const hasAlert = faultTypes.length > 0
+
+  const parts = []
+  if (tilt) parts.push(`倾斜角度${tiltAngle}°超过阈值${angleThreshold}°`)
+  if (waterInfiltration) parts.push('水浸报警')
+  if (waterLevel) parts.push('水位超标报警')
+  if (lowBattery) parts.push(`电量仅剩${battery}%`)
+  if (lowSignal) parts.push(`信号强度${signal}%`)
+  if (offline) parts.push('设备离线')
+
+  let elapsedHours = 0
+  const faultTime = md.uploadTime || md.createTime
+  if (faultTime) {
+    const diff = Date.now() - new Date(faultTime).getTime()
+    elapsedHours = Math.max(0, Math.round(diff / (1000 * 60 * 60) * 10) / 10)
+  }
+
+  return {
+    id: raw?.equipmentId || raw?.equipmentCode || '',
+    code: raw?.equipmentCode || raw?.equipmentId || '',
+    equipmentId: raw?.equipmentId || '',
+    name: raw?.equipmentName || raw?.equipmentCode || '未命名设备',
+    typeId,
+    deviceTypeName: TYPE_MAP[typeId] || raw?.equipmentTypeName || '未知类型',
+    location: raw?.equipmentLocation || '暂无位置信息',
+    locationShort: (raw?.equipmentLocation || '暂无位置').slice(0, 20),
+    district: raw?.district || '',
+    battery, signal, temperature, tiltAngle, angleThreshold,
+    status: { tilt, waterInfiltration, waterLevel, lowBattery, lowSignal, offline },
+    faultTypes, hasAlert,
+    faultDesc: parts.join(';') || '设备状态正常',
+    abnormalTime: faultTime || '',
+    elapsedHours,
+    workOrderSubmitted: !!raw?.workOrderSubmitted
+  }
+}
+
+// ==================== 数据加载 ====================
+async function loadAll() {
+  loading.value = true
+  try {
+    const today = new Date()
+    const sixMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 5, 1)
+
+    // 日期范围:默认近6个月,后端统一计算全部统计数据
+    const startDate = dateRange.value?.[0] ? formatDate(new Date(dateRange.value[0])) : formatDate(sixMonthsAgo)
+    const endDate = dateRange.value?.[1] ? formatDate(new Date(dateRange.value[1])) : formatDate(today)
+
+    const { data } = await getManholeMaintenanceDashboard({ trendType: 2, startDate, endDate })
+
+    // 1. KPI 指标
+    const stat = data?.kpi || {}
+    kpi.total = stat.totalTerminal || 0
+    kpi.abnormal = stat.abnormalTerminal || 0
+    kpi.abnormalRate = stat.abnormalRate ?? '0.0'
+    kpi.submittedCount = stat.submittedCount || 0
+    kpi.solvedRate = stat.solvedRate ?? 0
+    kpi.totalCase = stat.totalCase || 0
+    kpi.disposalRate = stat.disposalRate ?? 0
+    kpi.delayRate = stat.delayRate ?? 0
+    kpi.avgResponseTime = stat.avgDisposalTime ?? '—'
+
+    // 2. 异常终端列表
+    abnormalDevices.value = (data?.abnormalDevices || []).map(normalizeDevice)
+
+    // 3. 渲染图表
+    await nextTick()
+    renderTrendChart(data?.abnormalTerminalTrend || [])
+    renderProblemTypeChart(data?.problemTypeDistribution || [])
+    renderDisposalTrendChart(data?.caseRateTrend || [])
+    renderRegionRankChart(data?.regionRank || [])
+  } catch (error) {
+    const msg = error?.message || error?.msg || '未知错误'
+    ElMessage.error('统计数据加载失败: ' + msg)
+  } finally {
+    loading.value = false
+  }
+}
+
+// ==================== ECharts 渲染 ====================
+function renderTrendChart(trendData) {
+  if (!trendChartRef.value) return
+  if (!trendChart) trendChart = echarts.init(trendChartRef.value)
+
+  const labels = (trendData || []).map(d => d.trendLabel || '')
+  const abnormalValues = (trendData || []).map(d => d.abnormalTerminal || 0)
+  const normalValues = (trendData || []).map(d => {
+    const abnormal = d.abnormalTerminal || 0
+    const total = d.totalTerminal || abnormal
+    return Math.max(0, total - abnormal)
+  })
+
+  trendChart.setOption({
+    tooltip: { trigger: 'axis' },
+    legend: { data: ['异常终端数', '正常终端数'] },
+    grid: { left: '8%', right: '5%', bottom: '12%', top: '15%' },
+    xAxis: { type: 'category', data: labels.length ? labels : ['暂无数据'] },
+    yAxis: { type: 'value', name: '数量' },
+    series: [
+      { name: '异常终端数', type: 'line', data: abnormalValues, smooth: true, lineStyle: { color: '#f56c6c' }, areaStyle: { opacity: 0.1 }, itemStyle: { color: '#f56c6c' } },
+      { name: '正常终端数', type: 'line', data: normalValues, smooth: true, lineStyle: { color: '#67c23a' }, areaStyle: { opacity: 0.1 }, itemStyle: { color: '#67c23a' } }
+    ]
+  })
+}
+
+function renderProblemTypeChart(distribution) {
+  if (!problemTypeChartRef.value) return
+  if (!problemTypeChart) problemTypeChart = echarts.init(problemTypeChartRef.value)
+
+  const colors = ['#f56c6c', '#e6a23c', '#409eff', '#909399', '#67c23a', '#9966ff']
+  const data = (distribution || []).map((item, i) => ({
+    name: item.name || '其他',
+    value: item.value || 0,
+    itemStyle: { color: colors[i % colors.length] }
+  }))
+
+  problemTypeChart.setOption({
+    tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
+    legend: { orient: 'vertical', left: 'left', textStyle: { fontSize: 11 } },
+    series: [{
+      type: 'pie', radius: '55%', center: ['55%', '50%'],
+      data: data.length ? data : [{ name: '暂无数据', value: 0 }],
+      label: { show: true, formatter: '{b}: {d}%' }
+    }]
+  })
+}
+
+function renderDisposalTrendChart(trendData) {
+  if (!disposalTrendChartRef.value) return
+  if (!disposalTrendChart) disposalTrendChart = echarts.init(disposalTrendChartRef.value)
+
+  const labels = (trendData || []).map(d => d.trendLabel || '')
+  const disposalRates = (trendData || []).map(d => Number(d.caseDisposalRate) || 0)
+  const delayRates = (trendData || []).map(d => Number(d.caseDelayRate) || 0)
+
+  disposalTrendChart.setOption({
+    tooltip: { trigger: 'axis' },
+    legend: { data: ['处置率(%)', '延期率(%)'] },
+    grid: { left: '8%', right: '5%', bottom: '12%', top: '15%' },
+    xAxis: { type: 'category', data: labels.length ? labels : ['暂无数据'] },
+    yAxis: { type: 'value', name: '百分比(%)', max: 100 },
+    series: [
+      { name: '处置率(%)', type: 'line', data: disposalRates, smooth: true, itemStyle: { color: '#67c23a' } },
+      { name: '延期率(%)', type: 'line', data: delayRates, smooth: true, itemStyle: { color: '#e6a23c' } }
+    ]
+  })
+}
+
+function renderRegionRankChart(regionRank) {
+  if (!regionRankChartRef.value) return
+  if (!regionRankChart) regionRankChart = echarts.init(regionRankChartRef.value)
+
+  const entries = (regionRank || []).slice(0, 8)
+
+  regionRankChart.setOption({
+    tooltip: {
+      trigger: 'axis', axisPointer: { type: 'shadow' },
+      formatter: (params) => {
+        const p = params[0]
+        const item = entries[p.dataIndex]
+        return item ? `${p.name}: 异常 ${p.value} / 总数 ${item.total}` : p.name
+      }
+    },
+    grid: { left: '15%', right: '8%', bottom: '8%', top: '8%' },
+    xAxis: { type: 'value', name: '异常数量' },
+    yAxis: { type: 'category', data: entries.map(e => e.name), axisLabel: { fontSize: 11 } },
+    series: [{
+      name: '异常数量', type: 'bar',
+      data: entries.map(e => e.abnormal),
+      itemStyle: { color: '#409eff', borderRadius: [0, 4, 4, 0] },
+      label: { show: true, position: 'right', fontSize: 11 }
+    }]
+  })
+}
+
+// ==================== 计算属性 ====================
 const filteredAbnormalDevices = computed(() => {
   let list = abnormalDevices.value
   if (searchKeyword.value) {
     const kw = searchKeyword.value.toLowerCase()
-    list = list.filter(d => d.code.toLowerCase().includes(kw) || d.name.toLowerCase().includes(kw) || d.locationShort.toLowerCase().includes(kw))
+    list = list.filter(d =>
+      d.code.toLowerCase().includes(kw) ||
+      d.name.toLowerCase().includes(kw) ||
+      d.location.toLowerCase().includes(kw)
+    )
   }
   if (faultTypeFilter.value) {
-    list = list.filter(d => d.faultType === faultTypeFilter.value)
+    list = list.filter(d => d.faultTypes.some(ft => ft.type === faultTypeFilter.value))
   }
   return list
 })
+
 const paginatedAbnormalDevices = computed(() => {
   const start = (currentPage.value - 1) * pageSize.value
   return filteredAbnormalDevices.value.slice(start, start + pageSize.value)
 })
 
-// 趋势图数据(模拟)
-const trendChartOption = computed(() => {
-  const months = ['1月', '2月', '3月', '4月', '5月', '6月']
-  const problems = [12, 15, 18, 22, 25, 20]
-  const solved = [8, 11, 14, 18, 21, 18]
-  return {
-    tooltip: { trigger: 'axis' },
-    legend: { data: ['新增问题数', '解决问题数'] },
-    xAxis: { type: 'category', data: months },
-    yAxis: { type: 'value', name: '数量' },
-    series: [
-      { name: '新增问题数', type: 'line', data: problems, smooth: true, lineStyle: { color: '#f56c6c' }, areaStyle: { opacity: 0.1 } },
-      { name: '解决问题数', type: 'line', data: solved, smooth: true, lineStyle: { color: '#67c23a' }, areaStyle: { opacity: 0.1 } }
-    ]
-  }
-})
-
-// 问题类型分布饼图
-const problemTypeOption = computed(() => ({
-  tooltip: { trigger: 'item' },
-  legend: { orient: 'vertical', left: 'left' },
-  series: [{
-    type: 'pie', radius: '55%', center: ['50%', '50%'],
-    data: [
-      { name: '倾斜超标', value: 32, itemStyle: { color: '#f56c6c' } },
-      { name: '水位超限', value: 25, itemStyle: { color: '#e6a23c' } },
-      { name: '异常震动', value: 18, itemStyle: { color: '#409eff' } },
-      { name: '通讯故障', value: 15, itemStyle: { color: '#909399' } },
-      { name: '电池低电量', value: 10, itemStyle: { color: '#67c23a' } }
-    ],
-    label: { show: true, formatter: '{b}: {d}%' }
-  }]
-}))
-
-// 处置率 vs 延期率趋势
-const disposalTrendOption = computed(() => ({
-  tooltip: { trigger: 'axis' },
-  legend: { data: ['处置率(%)', '延期率(%)'] },
-  xAxis: { type: 'category', data: ['1月', '2月', '3月', '4月', '5月', '6月'] },
-  yAxis: { type: 'value', name: '百分比(%)' },
-  series: [
-    { name: '处置率(%)', type: 'line', data: [85, 88, 90, 91, 93, 94], lineStyle: { color: '#67c23a' }, smooth: true },
-    { name: '延期率(%)', type: 'line', data: [12, 11, 10, 9.5, 9, 8.5], lineStyle: { color: '#e6a23c' }, smooth: true }
-  ]
-}))
-
-// 区域运维排行
-const regionRankOption = computed(() => ({
-  tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
-  xAxis: { type: 'value', name: '问题数量' },
-  yAxis: { type: 'category', data: ['浦东新区', '徐汇区', '黄浦区', '静安区', '杨浦区'], axisLabel: { rotate: 0 } },
-  series: [{ name: '问题数量', type: 'bar', data: [28, 22, 18, 15, 12], itemStyle: { color: '#409eff', borderRadius: [0, 4, 4, 0] } }]
-}))
-
+// ==================== 辅助函数 ====================
 const getFaultTagType = (type) => {
-  const map = { tilt: 'danger', water: 'danger', vibration: 'warning', communication: 'info', battery: 'warning' }
+  const map = { tilt: 'danger', waterLevel: 'danger', waterInfiltration: 'danger', offline: 'info', battery: 'warning', signal: 'warning' }
   return map[type] || 'info'
 }
-const getStatusTag = (status) => {
-  const map = { pending: 'danger', processing: 'warning', completed: 'success' }
-  return map[status] || 'info'
+
+const viewDetail = (device) => {
+  currentDevice.value = device
+  detailDrawerVisible.value = true
 }
-const getStatusText = (status) => {
-  const map = { pending: '待处理', processing: '处理中', completed: '已完成' }
-  return map[status] || status
+
+// ==================== 生命周期 ====================
+const handleResize = () => {
+  trendChart && trendChart.resize()
+  problemTypeChart && problemTypeChart.resize()
+  disposalTrendChart && disposalTrendChart.resize()
+  regionRankChart && regionRankChart.resize()
 }
 
-const refreshData = () => { ElMessage.success('数据已刷新') }
-const refreshList = () => { currentPage.value = 1; ElMessage.success('列表已刷新') }
-const exportReport = () => { ElMessage.success('导出运维报表(演示)') }
-const viewDetail = (device) => { currentDevice.value = device; detailDrawerVisible.value = true }
+onMounted(async () => {
+  await nextTick()
+  window.addEventListener('resize', handleResize)
+  loadAll()
+})
 
-import { ElMessage } from 'element-plus'
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', handleResize)
+  trendChart && trendChart.dispose()
+  problemTypeChart && problemTypeChart.dispose()
+  disposalTrendChart && disposalTrendChart.dispose()
+  regionRankChart && regionRankChart.dispose()
+})
 </script>
 
 <style scoped>
@@ -420,4 +596,30 @@ import { ElMessage } from 'element-plus'
 .device-detail { padding: 8px; }
 .sub-title { font-weight: 600; margin: 16px 0 12px 0; }
 .over-time { color: #f56c6c; font-weight: 600; }
-</style>
+
+.monitor-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 10px;
+}
+.monitor-item {
+  background: #f5f7fa;
+  border-radius: 8px;
+  padding: 10px;
+  text-align: center;
+  border: 1px solid #e9ecef;
+}
+.monitor-item.alert {
+  background: #fef0f0;
+  border-color: #f56c6c;
+}
+.m-label { font-size: 12px; color: #909399; }
+.m-value { font-size: 16px; font-weight: 600; color: #303133; margin: 4px 0; }
+.monitor-item.alert .m-value { color: #f56c6c; }
+.m-extra { font-size: 11px; color: #aaa; }
+
+@media (max-width: 1200px) {
+  .kpi-cards { grid-template-columns: repeat(3, 1fr); }
+  .charts-row { grid-template-columns: 1fr; }
+}
+</style>

+ 480 - 311
src/views/subSystem/manholeCover/operation/operationTask.vue

@@ -1,27 +1,29 @@
-<!--运维任务管理-->
+<!--运维任务管理 - 窨井设备异常派发中心-->
 <template>
   <div class="terminal-dispatch">
     <!-- 页面头部 -->
     <div class="page-header">
       <div class="header-left">
         <el-icon><Warning /></el-icon>
-        <span class="title">异常终端派发中心</span>
+        <span class="title">窨井设备异常派发中心</span>
         <el-tag type="danger" effect="dark">未处理异常 {{ abnormalCount }}</el-tag>
       </div>
       <div class="header-right">
         <el-input
-            v-model="searchKeyword"
-            placeholder="搜索终端编号/名称/地址"
-            clearable
-            :prefix-icon="Search"
-            style="width: 240px"
+          v-model="searchKeyword"
+          placeholder="搜索设备编号/名称/位置"
+          clearable
+          :prefix-icon="Search"
+          style="width: 240px"
+          @keyup.enter="currentPage = 1"
+          @clear="currentPage = 1"
         />
-        <el-select v-model="deviceTypeFilter" placeholder="设备类型" clearable style="width: 120px">
-          <el-option label="智能井盖" value="smart" />
-          <el-option label="防爆井盖" value="explosion" />
-          <el-option label="水位计" value="water" />
+        <el-select v-model="deviceTypeFilter" placeholder="设备类型" clearable style="width: 130px" @change="currentPage = 1">
+          <el-option label="供水窨井" value="5" />
+          <el-option label="排水窨井" value="9" />
+          <el-option label="燃气窨井" value="10" />
         </el-select>
-        <el-button type="primary" :icon="Refresh" @click="refreshList">刷新</el-button>
+        <el-button type="primary" :icon="Refresh" @click="loadDevices" :loading="loading">刷新</el-button>
       </div>
     </div>
 
@@ -40,141 +42,184 @@
         <div class="stat-label">待派发</div>
       </div>
       <div class="stat-card">
-        <div class="stat-value">{{ todayDispatched }}</div>
-        <div class="stat-label">今日已派发</div>
+        <div class="stat-value">{{ dispatchedCount }}</div>
+        <div class="stat-label">已派发</div>
       </div>
     </div>
 
     <!-- 批量操作栏 -->
     <div class="batch-bar" v-if="selectedRows.length > 0">
-      <span>已选择 <strong>{{ selectedRows.length }}</strong> 个异常终端</span>
-      <el-button type="primary" @click="batchDispatch">批量派发</el-button>
+      <span>已选择 <strong>{{ selectedRows.length }}</strong> 个异常设备</span>
+      <el-button type="primary" @click="batchDispatch">批量提交工单</el-button>
       <el-button @click="clearSelection">取消选择</el-button>
     </div>
 
-    <!-- 主内容区:左侧终端列表 + 右侧详情 -->
+    <!-- 主内容区:左侧异常设备列表 + 右侧详情 -->
     <div class="main-layout">
-      <!-- 左侧异常终端列表 -->
+      <!-- 左侧异常设备列表 -->
       <div class="terminal-list-panel">
         <div class="list-header">
-          <span>异常终端列表(未处理)</span>
+          <span>异常设备列表({{ filteredTerminals.length }} 台)</span>
           <el-checkbox v-model="selectAll" @change="handleSelectAll">全选</el-checkbox>
         </div>
-        <div class="terminal-list">
+        <div class="terminal-list" v-loading="loading">
           <div
-              v-for="terminal in paginatedTerminals"
-              :key="terminal.id"
-              class="terminal-item"
-              :class="{
-              selected: selectedRows.includes(terminal.id),
+            v-for="terminal in paginatedTerminals"
+            :key="terminal.id"
+            class="terminal-item"
+            :class="{
+              selected: selectedTerminal?.id === terminal.id,
               critical: terminal.abnormalLevel === 'critical',
               warning: terminal.abnormalLevel === 'warning'
             }"
+            @click="selectTerminal(terminal)"
           >
             <el-checkbox
-                :model-value="selectedRows.includes(terminal.id)"
-                @change="(val) => toggleSelect(terminal.id, val)"
-                class="select-checkbox"
+              :model-value="selectedRows.includes(terminal.id)"
+              @change="(val) => toggleSelect(terminal.id, val)"
+              class="select-checkbox"
+              @click.stop
             />
-            <div class="terminal-content" @click="selectTerminal(terminal)">
+            <div class="terminal-content">
               <div class="terminal-header">
                 <span class="terminal-code">{{ terminal.code }}</span>
                 <el-tag :type="terminal.abnormalLevel === 'critical' ? 'danger' : 'warning'" size="small">
                   {{ terminal.abnormalLevel === 'critical' ? '严重' : '一般' }}
                 </el-tag>
               </div>
+              <div class="terminal-name">{{ terminal.name }}</div>
               <div class="terminal-location">
                 <el-icon><Location /></el-icon> {{ terminal.locationShort }}
               </div>
               <div class="terminal-desc">{{ terminal.abnormalDesc }}</div>
-              <div class="terminal-time">异常时间: {{ terminal.abnormalTime }}</div>
+              <div class="terminal-time">异常时间: {{ terminal.abnormalTime || '—' }}</div>
+              <div class="terminal-footer">
+                <el-tag v-if="terminal.workOrderSubmitted" type="success" size="small" effect="plain">已派发</el-tag>
+                <el-tag v-else type="danger" size="small" effect="plain">待派发</el-tag>
+                <span class="type-tag">{{ terminal.deviceTypeName }}</span>
+              </div>
             </div>
           </div>
+          <el-empty v-if="!loading && filteredTerminals.length === 0" description="暂无异常设备" :image-size="80" />
         </div>
-        <div class="pagination-area">
+        <div class="pagination-area" v-if="filteredTerminals.length > pageSize">
           <el-pagination
-              small
-              layout="prev, pager, next"
-              :total="filteredTerminals.length"
-              :page-size="pageSize"
-              v-model:current-page="currentPage"
+            small
+            layout="prev, pager, next"
+            :total="filteredTerminals.length"
+            :page-size="pageSize"
+            v-model:current-page="currentPage"
           />
         </div>
       </div>
 
       <!-- 右侧详情面板 -->
       <div class="detail-panel" v-if="selectedTerminal">
-        <el-tabs v-model="activeTab">
-          <!-- 终端基本信息与位置 -->
-          <el-tab-pane label="终端信息" name="info">
+        <el-tabs v-model="activeTab" @tab-change="onTabChange">
+          <!-- 设备信息 -->
+          <el-tab-pane label="设备信息" name="info">
             <div class="info-section">
               <div class="section-title">
-                <span><el-icon><Monitor /></el-icon> 终端基本信息</span>
-                <el-button type="primary" size="small" @click="openDispatchDialog(selectedTerminal)">派发任务</el-button>
+                <span><el-icon><Monitor /></el-icon> 设备基本信息</span>
+                <el-button
+                  type="primary"
+                  size="small"
+                  @click="openDispatchDialog(selectedTerminal)"
+                  :disabled="selectedTerminal.workOrderSubmitted"
+                >
+                  {{ selectedTerminal.workOrderSubmitted ? '已派发' : '提交工单' }}
+                </el-button>
               </div>
               <el-descriptions :column="2" border>
-                <el-descriptions-item label="终端编号">{{ selectedTerminal.code }}</el-descriptions-item>
+                <el-descriptions-item label="设备编号">{{ selectedTerminal.code }}</el-descriptions-item>
                 <el-descriptions-item label="设备名称">{{ selectedTerminal.name }}</el-descriptions-item>
                 <el-descriptions-item label="设备类型">{{ selectedTerminal.deviceTypeName }}</el-descriptions-item>
-                <el-descriptions-item label="设备型号">{{ selectedTerminal.model }}</el-descriptions-item>
-                <el-descriptions-item label="权属单位">{{ selectedTerminal.ownerUnit }}</el-descriptions-item>
+                <el-descriptions-item label="设备位置">{{ selectedTerminal.location }}</el-descriptions-item>
                 <el-descriptions-item label="异常级别">
                   <el-tag :type="selectedTerminal.abnormalLevel === 'critical' ? 'danger' : 'warning'">
                     {{ selectedTerminal.abnormalLevel === 'critical' ? '严重异常' : '一般异常' }}
                   </el-tag>
                 </el-descriptions-item>
-                <el-descriptions-item label="异常描述" :span="2">{{ selectedTerminal.abnormalDesc }}</el-descriptions-item>
-                <el-descriptions-item label="异常时间">{{ selectedTerminal.abnormalTime }}</el-descriptions-item>
-                <el-descriptions-item label="当前状态">
-                  <el-tag :type="selectedTerminal.status === 'pending' ? 'danger' : 'success'">
-                    {{ selectedTerminal.status === 'pending' ? '待处理' : '处理中' }}
+                <el-descriptions-item label="派发状态">
+                  <el-tag :type="selectedTerminal.workOrderSubmitted ? 'success' : 'danger'">
+                    {{ selectedTerminal.workOrderSubmitted ? '已派发' : '待派发' }}
                   </el-tag>
                 </el-descriptions-item>
+                <el-descriptions-item label="异常描述" :span="2">{{ selectedTerminal.abnormalDesc }}</el-descriptions-item>
               </el-descriptions>
             </div>
 
-            <!-- 位置信息 - GIS地图模拟 -->
-            <div class="location-section">
-              <div class="section-title"><el-icon><MapLocation /></el-icon> 终端位置信息</div>
-              <div class="map-container">
-                <div class="mock-map">
-                  <el-icon :size="40" color="#f56c6c"><Location /></el-icon>
-                  <div class="map-coords">
-                    <div><strong>{{ selectedTerminal.address }}</strong></div>
-                    <div>经度: {{ selectedTerminal.lng }}, 纬度: {{ selectedTerminal.lat }}</div>
-                  </div>
+            <!-- 实时监测数据 -->
+            <div class="info-section">
+              <div class="section-title"><el-icon><DataAnalysis /></el-icon> 实时监测数据</div>
+              <div class="monitor-grid">
+                <div class="monitor-item" :class="{ alert: selectedTerminal.status.tilt }">
+                  <div class="monitor-label">倾斜角度</div>
+                  <div class="monitor-value">{{ selectedTerminal.tiltAngle }}°</div>
+                  <div class="monitor-extra">阈值: {{ selectedTerminal.threshold }}°</div>
+                </div>
+                <div class="monitor-item" :class="{ alert: selectedTerminal.status.waterInfiltration }">
+                  <div class="monitor-label">水浸状态</div>
+                  <div class="monitor-value">{{ selectedTerminal.status.waterInfiltration ? '报警' : '正常' }}</div>
+                </div>
+                <div class="monitor-item" :class="{ alert: selectedTerminal.status.waterLevel }">
+                  <div class="monitor-label">水位状态</div>
+                  <div class="monitor-value">{{ selectedTerminal.status.waterLevel ? '超标' : '正常' }}</div>
+                </div>
+                <div class="monitor-item" :class="{ alert: selectedTerminal.status.lowBattery }">
+                  <div class="monitor-label">电量</div>
+                  <div class="monitor-value">{{ selectedTerminal.battery }}%</div>
+                </div>
+                <div class="monitor-item" :class="{ alert: selectedTerminal.status.lowSignal }">
+                  <div class="monitor-label">信号强度</div>
+                  <div class="monitor-value">{{ selectedTerminal.signal }}%</div>
+                </div>
+                <div class="monitor-item">
+                  <div class="monitor-label">温度</div>
+                  <div class="monitor-value">{{ selectedTerminal.temperature || '—' }}°C</div>
                 </div>
               </div>
+              <div class="last-upload">最后上传时间: {{ selectedTerminal.lastTime || '—' }}</div>
+            </div>
+
+            <!-- 位置信息 -->
+            <div class="location-section">
+              <div class="section-title"><el-icon><MapLocation /></el-icon> 设备位置信息</div>
+              <div class="location-info">
+                <el-icon :size="20" color="#f56c6c"><Location /></el-icon>
+                <span>{{ selectedTerminal.location }}</span>
+                <span v-if="selectedTerminal.lng" class="coords">(经度: {{ selectedTerminal.lng }}, 纬度: {{ selectedTerminal.lat }})</span>
+              </div>
             </div>
           </el-tab-pane>
 
-          <!-- 历次处置记录 -->
+          <!-- 处置记录 -->
           <el-tab-pane label="处置记录" name="history">
             <div class="history-section">
-              <div class="section-title"><el-icon><Document /></el-icon> 历次处置信息</div>
-              <el-timeline v-if="selectedTerminal.historyRecords?.length">
-                <el-timeline-item
-                    v-for="record in selectedTerminal.historyRecords"
-                    :key="record.id"
-                    :timestamp="record.time"
-                    :type="record.type"
-                >
-                  <div class="record-content">
-                    <div><strong>派发对象:</strong> {{ record.dispatchedTo }}</div>
-                    <div><strong>处置说明:</strong> {{ record.description }}</div>
-                    <div v-if="record.feedbackImages?.length" class="record-images">
-                      <el-image
-                          v-for="(img, idx) in record.feedbackImages"
-                          :key="idx"
-                          :src="img"
-                          fit="cover"
-                          class="history-img"
-                      />
+              <div class="section-title"><el-icon><Document /></el-icon> 工单处置记录</div>
+              <div v-loading="loadingOrders">
+                <el-timeline v-if="workOrders.length">
+                  <el-timeline-item
+                    v-for="order in workOrders"
+                    :key="order.orderId"
+                    :timestamp="order.createTime || ''"
+                    :type="Number(order.orderStatus) >= 6 ? 'success' : 'primary'"
+                  >
+                    <div class="record-content">
+                      <div class="record-row"><strong>工单编号:</strong> {{ order.orderNo || '—' }}</div>
+                      <div class="record-row"><strong>工单类型:</strong> {{ orderTypeText(order.orderType) }}</div>
+                      <div class="record-row"><strong>优先级:</strong>
+                        <el-tag :type="orderLevelTagType(order.orderLevel)" size="small">{{ priorityText(order.orderLevel) }}</el-tag>
+                      </div>
+                      <div class="record-row"><strong>状态:</strong>
+                        <el-tag :type="orderStatusTagType(order.orderStatus)" size="small">{{ orderStatusText(order.orderStatus) }}</el-tag>
+                      </div>
+                      <div class="record-row" v-if="order.orderDesc"><strong>描述:</strong> {{ order.orderDesc }}</div>
                     </div>
-                  </div>
-                </el-timeline-item>
-              </el-timeline>
-              <el-empty v-else description="暂无历史处置记录" :image-size="80" />
+                  </el-timeline-item>
+                </el-timeline>
+                <el-empty v-else description="暂无工单处置记录" :image-size="80" />
+              </div>
             </div>
           </el-tab-pane>
         </el-tabs>
@@ -182,185 +227,242 @@
 
       <!-- 未选中状态 -->
       <div v-else class="empty-panel">
-        <el-empty description="请从左侧选择异常终端查看详情" :image-size="140" />
+        <el-empty description="请从左侧选择异常设备查看详情" :image-size="140" />
       </div>
     </div>
 
-    <!-- 派发对话框 -->
-    <el-dialog v-model="dispatchDialogVisible" :title="`派发任务 - ${currentDispatchTerminal?.code}`" width="500px">
+    <!-- 提交工单对话框 -->
+    <el-dialog v-model="dispatchDialogVisible" title="提交工单" width="520px" @close="dispatchForm = { orderType: 1, orderLevel: 2, orderDesc: '', owner: '' }">
       <el-form :model="dispatchForm" label-width="100px">
-        <el-form-item label="派发对象" required>
-          <el-select v-model="dispatchForm.targetType" placeholder="选择派发类型" style="width: 100%" @change="onTargetTypeChange">
-            <el-option label="施工单位" value="construction" />
-            <el-option label="维护人员" value="maintainer" />
+        <el-form-item label="设备名称">
+          <el-input :model-value="currentDispatchTerminal?.name" disabled />
+        </el-form-item>
+        <el-form-item label="异常描述">
+          <el-input type="textarea" :model-value="currentDispatchTerminal?.abnormalDesc" disabled :rows="2" />
+        </el-form-item>
+        <el-form-item label="工单类型" required>
+          <el-select v-model="dispatchForm.orderType" placeholder="选择工单类型" style="width: 100%">
+            <el-option label="故障维修" :value="1" />
+            <el-option label="日常巡检" :value="2" />
+            <el-option label="设备保养" :value="3" />
           </el-select>
         </el-form-item>
-        <el-form-item label="选择单位/人员" required>
-          <el-select v-model="dispatchForm.targetId" placeholder="请选择" style="width: 100%">
-            <el-option
-                v-for="item in dispatchTargets"
-                :key="item.id"
-                :label="item.name"
-                :value="item.id"
-            />
+        <el-form-item label="优先级" required>
+          <el-select v-model="dispatchForm.orderLevel" placeholder="选择优先级" style="width: 100%">
+            <el-option label="紧急" :value="1" />
+            <el-option label="一般" :value="2" />
+            <el-option label="低" :value="3" />
           </el-select>
         </el-form-item>
-        <el-form-item label="派发说明">
-          <el-input type="textarea" v-model="dispatchForm.remark" placeholder="填写任务要求、注意事项等" rows="3" />
+        <el-form-item label="问题描述" required>
+          <el-input type="textarea" v-model="dispatchForm.orderDesc" placeholder="填写问题描述、维修要求等" :rows="3" />
         </el-form-item>
-        <el-form-item label="期望完成时间">
-          <el-date-picker v-model="dispatchForm.deadline" type="datetime" placeholder="选择截止时间" style="width: 100%" />
+        <el-form-item label="负责人">
+          <el-input v-model="dispatchForm.owner" placeholder="填写负责人姓名(选填)" />
         </el-form-item>
       </el-form>
       <template #footer>
         <el-button @click="dispatchDialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="confirmDispatch">确认派发</el-button>
+        <el-button type="primary" @click="confirmDispatch" :loading="submitting">确认提交</el-button>
       </template>
     </el-dialog>
 
-    <!-- 批量派发确认对话框 -->
-    <el-dialog v-model="batchDispatchDialogVisible" title="批量派发确认" width="450px">
-      <p>将向以下 {{ batchDispatchIds.length }} 个异常终端派发任务:</p>
+    <!-- 批量提交工单对话框 -->
+    <el-dialog v-model="batchDispatchDialogVisible" title="批量提交工单" width="450px">
+      <p>将向以下 {{ batchDispatchIds.length }} 个异常设备提交工单:</p>
       <div class="batch-list">
         <el-tag v-for="id in batchDispatchIds.slice(0, 5)" :key="id" size="small" style="margin: 4px">
           {{ getTerminalCode(id) }}
         </el-tag>
-        <span v-if="batchDispatchIds.length > 5">等{{ batchDispatchIds.length }}个终端</span>
+        <span v-if="batchDispatchIds.length > 5">等{{ batchDispatchIds.length }}个设备</span>
       </div>
-      <el-form style="margin-top: 16px">
-        <el-form-item label="派发对象">
-          <el-select v-model="batchDispatchForm.targetType" placeholder="选择派发类型" style="width: 100%">
-            <el-option label="施工单位" value="construction" />
-            <el-option label="维护人员" value="maintainer" />
+      <el-form style="margin-top: 16px" label-width="80px">
+        <el-form-item label="工单类型">
+          <el-select v-model="batchDispatchForm.orderType" style="width: 100%">
+            <el-option label="故障维修" :value="1" />
+            <el-option label="日常巡检" :value="2" />
+            <el-option label="设备保养" :value="3" />
           </el-select>
         </el-form-item>
-        <el-form-item label="选择单位/人员">
-          <el-select v-model="batchDispatchForm.targetId" placeholder="请选择" style="width: 100%">
-            <el-option v-for="item in batchDispatchTargets" :key="item.id" :label="item.name" :value="item.id" />
+        <el-form-item label="优先级">
+          <el-select v-model="batchDispatchForm.orderLevel" style="width: 100%">
+            <el-option label="紧急" :value="1" />
+            <el-option label="一般" :value="2" />
+            <el-option label="低" :value="3" />
           </el-select>
         </el-form-item>
+        <el-form-item label="问题描述">
+          <el-input type="textarea" v-model="batchDispatchForm.orderDesc" :rows="2" placeholder="批量工单描述" />
+        </el-form-item>
       </el-form>
       <template #footer>
         <el-button @click="batchDispatchDialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="confirmBatchDispatch">确认批量派发</el-button>
+        <el-button type="primary" @click="confirmBatchDispatch" :loading="submitting">确认批量提交</el-button>
       </template>
     </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
-import { Warning, Search, Refresh, Location, Monitor, MapLocation, Document } from '@element-plus/icons-vue'
+import { ref, computed, onMounted } from 'vue'
+import { Warning, Search, Refresh, Location, Monitor, MapLocation, Document, DataAnalysis } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
-
-// 模拟异常终端数据
-const mockTerminals = ref([
-  {
-    id: 1, code: 'TM-1001', name: '人民路智能井盖', deviceType: 'smart', deviceTypeName: '智能井盖',
-    model: 'WATCHMAN-S2', ownerUnit: '市政工程管理处', abnormalLevel: 'critical',
-    abnormalDesc: '倾斜角度14.2°,超过阈值8°,有跌落风险', abnormalTime: '2025-04-02 14:23:15',
-    status: 'pending', lng: 121.487, lat: 31.249, address: '人民路与解放路口东50m', locationShort: '人民路口',
-    historyRecords: []
-  },
-  {
-    id: 2, code: 'TM-1002', name: '滨江路防爆井盖', deviceType: 'explosion', deviceTypeName: '防爆井盖',
-    model: 'WATCHMAN-EX', ownerUnit: '水务集团', abnormalLevel: 'critical',
-    abnormalDesc: '水位62cm,超过阈值40cm,溢水风险', abnormalTime: '2025-04-02 13:55:02',
-    status: 'pending', lng: 121.502, lat: 31.235, address: '滨江路化工园区南门', locationShort: '滨江园区',
-    historyRecords: []
-  },
-  {
-    id: 3, code: 'TM-1003', name: '古城街井盖', deviceType: 'smart', deviceTypeName: '智能井盖',
-    model: 'WATCHMAN-S1', ownerUnit: '市政设施管理处', abnormalLevel: 'warning',
-    abnormalDesc: '异常震动112mg,超过阈值80mg', abnormalTime: '2025-04-02 12:10:33',
-    status: 'pending', lng: 121.478, lat: 31.258, address: '古城街北段树下', locationShort: '古城街',
-    historyRecords: [
-      { id: 1, time: '2025-03-28 10:30:00', type: 'primary', dispatchedTo: '维修一组-张建国', description: '现场检查为车辆碾压导致,已加固' }
-    ]
-  },
-  {
-    id: 4, code: 'TM-1004', name: '南港路井盖', deviceType: 'smart', deviceTypeName: '智能井盖',
-    model: 'WATCHMAN-S2', ownerUnit: '市政工程管理处', abnormalLevel: 'warning',
-    abnormalDesc: '井盖异常开启', abnormalTime: '2025-04-02 11:45:20',
-    status: 'pending', lng: 121.495, lat: 31.228, address: '南港路旧车站段', locationShort: '南港路',
-    historyRecords: []
-  },
-  {
-    id: 5, code: 'TM-1005', name: '石化区防爆井盖', deviceType: 'explosion', deviceTypeName: '防爆井盖',
-    model: 'WATCHMAN-EX', ownerUnit: '生态环境局', abnormalLevel: 'critical',
-    abnormalDesc: 'H₂S浓度28ppm,超过阈值15ppm', abnormalTime: '2025-04-02 10:30:45',
-    status: 'pending', lng: 121.524, lat: 31.239, address: '石化大道南侧', locationShort: '石化大道',
-    historyRecords: [
-      { id: 1, time: '2025-03-30 09:00:00', type: 'success', dispatchedTo: '应急抢修队-赵志远', description: '已通风处理,浓度降至安全范围' }
-    ]
-  },
-  {
-    id: 6, code: 'TM-1006', name: '高新区智能井盖', deviceType: 'smart', deviceTypeName: '智能井盖',
-    model: 'WATCHMAN-S2', ownerUnit: '市政设施管理处', abnormalLevel: 'warning',
-    abnormalDesc: '电池电量仅剩12%,需及时更换', abnormalTime: '2025-04-02 08:45:00',
-    status: 'pending', lng: 121.512, lat: 31.242, address: '高新大道云计算中心旁', locationShort: '高新区',
-    historyRecords: []
-  }
-])
-
-// 派发目标选项
-const constructionUnits = [
-  { id: 'c1', name: '市政一建施工队', type: 'construction' },
-  { id: 'c2', name: '水务工程公司', type: 'construction' },
-  { id: 'c3', name: '管道抢修中心', type: 'construction' }
-]
-const maintainers = [
-  { id: 'm1', name: '张建国 - 维修一组', type: 'maintainer' },
-  { id: 'm2', name: '李志强 - 维修二组', type: 'maintainer' },
-  { id: 'm3', name: '王大明 - 应急抢修队', type: 'maintainer' },
-  { id: 'm4', name: '陈辉 - 巡检专员', type: 'maintainer' }
-]
-
-// 筛选条件
+import { getManholeAlarmMonitorList, addManholeWorkOrder, getManholeWorkOrderProcessingPage } from '@/api/pipeNetwork/basic'
+
+// ==================== 常量 ====================
+const TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
+const LOW_BATTERY_THRESHOLD = 15
+const LOW_SIGNAL_THRESHOLD = 20
+
+// ==================== 状态 ====================
+const loading = ref(false)
+const loadingOrders = ref(false)
+const submitting = ref(false)
+const devices = ref([])
 const searchKeyword = ref('')
 const deviceTypeFilter = ref('')
 const currentPage = ref(1)
-const pageSize = 6
+const pageSize = 8
 
-// 选择相关
 const selectedRows = ref([])
 const selectAll = ref(false)
-const selectedTerminal = ref(mockTerminals.value[0])
+const selectedTerminal = ref(null)
 const activeTab = ref('info')
 
-// 派发相关
+const workOrders = ref([])
+
 const dispatchDialogVisible = ref(false)
 const currentDispatchTerminal = ref(null)
-const dispatchForm = ref({
-  targetType: 'construction',
-  targetId: '',
-  remark: '',
-  deadline: ''
-})
+const dispatchForm = ref({ orderType: 1, orderLevel: 2, orderDesc: '', owner: '' })
+
 const batchDispatchDialogVisible = ref(false)
 const batchDispatchIds = ref([])
-const batchDispatchForm = ref({
-  targetType: 'construction',
-  targetId: ''
-})
+const batchDispatchForm = ref({ orderType: 1, orderLevel: 2, orderDesc: '' })
+
+// ==================== 数据加载 ====================
+async function loadDevices() {
+  loading.value = true
+  try {
+    const keyword = searchKeyword.value?.trim()
+    const res = await getManholeAlarmMonitorList({ keyword: keyword || undefined })
+    const result = res.data || {}
+    const allDevices = (result.devices || []).map(item => normalizeDevice(item))
+    // 只保留有异常的设备
+    devices.value = allDevices.filter(d => d.hasAlert)
+    // 默认选中第一个
+    if (devices.value.length && !selectedTerminal.value) {
+      selectedTerminal.value = devices.value[0]
+    } else if (devices.value.length === 0) {
+      selectedTerminal.value = null
+    }
+  } catch (error) {
+    devices.value = []
+    const msg = error?.message || error?.msg || '未知错误'
+    ElMessage.error('设备数据加载失败: ' + msg)
+  } finally {
+    loading.value = false
+  }
+}
 
-// 计算属性
-const abnormalTerminals = computed(() => {
-  return mockTerminals.value.filter(t => t.status === 'pending')
-})
+async function loadWorkOrders(deviceId) {
+  if (!deviceId) return
+  loadingOrders.value = true
+  try {
+    const res = await getManholeWorkOrderProcessingPage({
+      deviceId,
+      pageNum: 1,
+      pageSize: 20
+    })
+    workOrders.value = res.data?.records || res.rows || []
+  } catch (error) {
+    workOrders.value = []
+    console.error('[operationTask] 工单记录加载失败:', error)
+  } finally {
+    loadingOrders.value = false
+  }
+}
 
+// ==================== 设备数据规范化 ====================
+function normalizeDevice(raw) {
+  const md = raw?.manholeData || {}
+  const st = raw?.equipmentStatus || {}
+  const typeId = raw?.equipmentTypeId || ''
+
+  // 数值解析
+  const tiltAngle = parseFloat(md.tiltAngle) || 0
+  const angleThreshold = parseFloat(md.angleAlarmThreshold) || 15
+  const battery = parseFloat(md.batteryLevel) || 0
+  const signal = parseFloat(md.signalStrength) || 0
+  const temperature = md.temperatureValue ? parseFloat(md.temperatureValue).toFixed(1) : null
+  const isOnline = Number(st.onlineStatus) === 1
+
+  // 异常判定
+  const tilt = isOnline && tiltAngle > angleThreshold
+  const waterInfiltration = isOnline && `${md.waterInfiltrationAlarmStatus}` === '1'
+  const waterLevel = isOnline && `${md.waterLevelAlarmStatus}` === '1'
+  const lowBattery = battery > 0 && battery <= LOW_BATTERY_THRESHOLD
+  const lowSignal = signal > 0 && signal <= LOW_SIGNAL_THRESHOLD
+
+  // 告警等级
+  const isCritical = tilt || waterInfiltration || waterLevel
+  const isWarning = !isCritical && (lowBattery || lowSignal)
+  const hasAlert = isCritical || isWarning
+  const alertLevel = isCritical ? 'critical' : (isWarning ? 'warning' : null)
+
+  // 异常描述
+  const parts = []
+  if (tilt) parts.push(`倾斜角度${tiltAngle}°超过阈值${angleThreshold}°`)
+  if (waterInfiltration) parts.push('水浸报警')
+  if (waterLevel) parts.push('水位超标报警')
+  if (lowBattery) parts.push(`电量仅剩${battery}%`)
+  if (lowSignal) parts.push(`信号强度${signal}%`)
+
+  // 推荐工单类型
+  let suggestedOrderType = 1
+  if (lowBattery) suggestedOrderType = 3
+  else if (lowSignal) suggestedOrderType = 2
+
+  return {
+    id: raw?.equipmentId || raw?.equipmentCode || '',
+    code: raw?.equipmentCode || raw?.equipmentId || '',
+    equipmentId: raw?.equipmentId || '',
+    name: raw?.equipmentName || raw?.equipmentCode || '未命名设备',
+    typeId,
+    deviceTypeName: TYPE_MAP[typeId] || raw?.equipmentTypeName || '未知类型',
+    location: raw?.equipmentLocation || '暂无位置信息',
+    locationShort: (raw?.equipmentLocation || '暂无位置').slice(0, 20),
+    lng: raw?.longitude ?? null,
+    lat: raw?.latitude ?? null,
+    battery,
+    signal,
+    temperature,
+    tiltAngle,
+    threshold: angleThreshold,
+    status: { tilt, waterInfiltration, waterLevel, lowBattery, lowSignal },
+    hasAlert,
+    alertLevel,
+    abnormalLevel: alertLevel,
+    abnormalDesc: parts.join(';') || '设备状态正常',
+    abnormalTime: md.uploadTime || md.createTime || '',
+    lastTime: md.uploadTime || md.createTime || '',
+    workOrderSubmitted: !!raw?.workOrderSubmitted,
+    suggestedOrderType
+  }
+}
+
+// ==================== 计算属性 ====================
 const filteredTerminals = computed(() => {
-  let list = abnormalTerminals.value
+  let list = devices.value
   if (searchKeyword.value) {
     const kw = searchKeyword.value.toLowerCase()
     list = list.filter(t =>
-        t.code.toLowerCase().includes(kw) ||
-        t.name.toLowerCase().includes(kw) ||
-        t.address.toLowerCase().includes(kw)
+      t.code.toLowerCase().includes(kw) ||
+      t.name.toLowerCase().includes(kw) ||
+      t.location.toLowerCase().includes(kw)
     )
   }
   if (deviceTypeFilter.value) {
-    list = list.filter(t => t.deviceType === deviceTypeFilter.value)
+    list = list.filter(t => t.typeId === deviceTypeFilter.value)
   }
   return list
 })
@@ -370,37 +472,29 @@ const paginatedTerminals = computed(() => {
   return filteredTerminals.value.slice(start, start + pageSize)
 })
 
-const abnormalCount = computed(() => abnormalTerminals.value.length)
-const criticalCount = computed(() => abnormalTerminals.value.filter(t => t.abnormalLevel === 'critical').length)
-const warningCount = computed(() => abnormalTerminals.value.filter(t => t.abnormalLevel === 'warning').length)
-const pendingCount = computed(() => abnormalTerminals.value.filter(t => t.status === 'pending').length)
-const todayDispatched = computed(() => {
-  // 模拟今日派发数量
-  return 3
-})
-
-// 派发目标动态计算
-const dispatchTargets = computed(() => {
-  if (dispatchForm.value.targetType === 'construction') {
-    return constructionUnits
-  } else {
-    return maintainers
-  }
-})
-
-const batchDispatchTargets = computed(() => {
-  if (batchDispatchForm.value.targetType === 'construction') {
-    return constructionUnits
-  } else {
-    return maintainers
-  }
-})
-
-// 方法
-const refreshList = () => {
-  ElMessage.success('列表已刷新')
+const abnormalCount = computed(() => devices.value.length)
+const criticalCount = computed(() => devices.value.filter(t => t.abnormalLevel === 'critical').length)
+const warningCount = computed(() => devices.value.filter(t => t.abnormalLevel === 'warning').length)
+const pendingCount = computed(() => devices.value.filter(t => !t.workOrderSubmitted).length)
+const dispatchedCount = computed(() => devices.value.filter(t => t.workOrderSubmitted).length)
+
+// ==================== 辅助函数 ====================
+const orderTypeText = (t) => ({ 1: '故障维修', 2: '日常巡检', 3: '设备保养' }[t] || '—')
+const priorityText = (l) => ({ 1: '紧急', 2: '一般', 3: '低' }[l] || '—')
+const orderStatusText = (s) => ({
+  1: '待派单', 2: '待接单', 3: '已接单', 4: '处理中',
+  5: '待验收', 6: '已完成', 7: '已关闭', 8: '延期审核', 9: '已归档'
+}[s] || '—')
+const orderLevelTagType = (l) => ({ 1: 'danger', 2: 'warning', 3: 'info' }[l] || 'info')
+const orderStatusTagType = (s) => {
+  const n = Number(s)
+  if (n >= 6) return 'success'
+  if (n >= 3) return 'primary'
+  if (n >= 1) return 'warning'
+  return 'info'
 }
 
+// ==================== 选择操作 ====================
 const handleSelectAll = (val) => {
   if (val) {
     selectedRows.value = paginatedTerminals.value.map(t => t.id)
@@ -411,13 +505,12 @@ const handleSelectAll = (val) => {
 
 const toggleSelect = (id, val) => {
   if (val) {
-    selectedRows.value.push(id)
+    if (!selectedRows.value.includes(id)) selectedRows.value.push(id)
   } else {
     selectedRows.value = selectedRows.value.filter(i => i !== id)
   }
-  // 更新全选状态
   selectAll.value = paginatedTerminals.value.length > 0 &&
-      paginatedTerminals.value.every(t => selectedRows.value.includes(t.id))
+    paginatedTerminals.value.every(t => selectedRows.value.includes(t.id))
 }
 
 const clearSelection = () => {
@@ -427,99 +520,126 @@ const clearSelection = () => {
 
 const selectTerminal = (terminal) => {
   selectedTerminal.value = terminal
+  if (activeTab.value === 'history') {
+    loadWorkOrders(terminal.equipmentId)
+  }
+}
+
+const onTabChange = (tabName) => {
+  if (tabName === 'history' && selectedTerminal.value) {
+    loadWorkOrders(selectedTerminal.value.equipmentId)
+  }
 }
 
 const getTerminalCode = (id) => {
-  const terminal = mockTerminals.value.find(t => t.id === id)
+  const terminal = devices.value.find(t => t.id === id)
   return terminal?.code || id
 }
 
-// 单个派发
+// ==================== 提交工单 ====================
 const openDispatchDialog = (terminal) => {
   currentDispatchTerminal.value = terminal
   dispatchForm.value = {
-    targetType: 'construction',
-    targetId: '',
-    remark: '',
-    deadline: ''
+    orderType: terminal.suggestedOrderType || 1,
+    orderLevel: terminal.abnormalLevel === 'critical' ? 1 : 2,
+    orderDesc: terminal.abnormalDesc || '',
+    owner: ''
   }
   dispatchDialogVisible.value = true
 }
 
-const confirmDispatch = () => {
-  if (!dispatchForm.value.targetId) {
-    ElMessage.warning('请选择派发对象')
+const confirmDispatch = async () => {
+  if (!dispatchForm.value.orderDesc?.trim()) {
+    ElMessage.warning('请填写问题描述')
     return
   }
-  const target = dispatchTargets.value.find(t => t.id === dispatchForm.value.targetId)
-  if (currentDispatchTerminal.value) {
-    // 更新终端状态
-    currentDispatchTerminal.value.status = 'processing'
-    // 添加处置记录
-    const newRecord = {
-      id: Date.now(),
-      time: new Date().toLocaleString(),
-      type: 'primary',
-      dispatchedTo: target.name,
-      description: dispatchForm.value.remark || '派发任务,请及时处理',
-      deadline: dispatchForm.value.deadline ? new Date(dispatchForm.value.deadline).toLocaleString() : null
-    }
-    if (!currentDispatchTerminal.value.historyRecords) {
-      currentDispatchTerminal.value.historyRecords = []
+  if (!currentDispatchTerminal.value?.equipmentId) {
+    ElMessage.warning('设备信息缺失,无法提交工单')
+    return
+  }
+  submitting.value = true
+  try {
+    const terminal = currentDispatchTerminal.value
+    const desc = dispatchForm.value.owner
+      ? `${dispatchForm.value.orderDesc}(负责人:${dispatchForm.value.owner})`
+      : dispatchForm.value.orderDesc
+    await addManholeWorkOrder({
+      deviceId: terminal.equipmentId,
+      orderType: dispatchForm.value.orderType,
+      orderLevel: dispatchForm.value.orderLevel,
+      orderDesc: desc
+    })
+    // 标记已派发
+    terminal.workOrderSubmitted = true
+    ElMessage.success('工单已提交,相关部门将即刻处理')
+    dispatchDialogVisible.value = false
+    // 如果在处置记录tab,刷新工单列表
+    if (activeTab.value === 'history') {
+      loadWorkOrders(terminal.equipmentId)
     }
-    currentDispatchTerminal.value.historyRecords.unshift(newRecord)
-
-    ElMessage.success(`已派发任务至 ${target.name}`)
+  } catch (error) {
+    const msg = error?.message || error?.msg || '未知错误'
+    ElMessage.error('工单提交失败: ' + msg)
+  } finally {
+    submitting.value = false
   }
-  dispatchDialogVisible.value = false
 }
 
-// 批量派发
+// ==================== 批量提交工单 ====================
 const batchDispatch = () => {
   if (selectedRows.value.length === 0) {
-    ElMessage.warning('请先选择异常终端')
+    ElMessage.warning('请先选择异常设备')
     return
   }
   batchDispatchIds.value = [...selectedRows.value]
-  batchDispatchForm.value = {
-    targetType: 'construction',
-    targetId: ''
-  }
+  batchDispatchForm.value = { orderType: 1, orderLevel: 2, orderDesc: '' }
   batchDispatchDialogVisible.value = true
 }
 
-const confirmBatchDispatch = () => {
-  if (!batchDispatchForm.value.targetId) {
-    ElMessage.warning('请选择派发对象')
+const confirmBatchDispatch = async () => {
+  if (!batchDispatchForm.value.orderDesc?.trim()) {
+    ElMessage.warning('请填写问题描述')
     return
   }
-  const target = batchDispatchTargets.value.find(t => t.id === batchDispatchForm.value.targetId)
-
-  // 批量更新所有选中的终端
-  batchDispatchIds.value.forEach(id => {
-    const terminal = mockTerminals.value.find(t => t.id === id)
-    if (terminal && terminal.status === 'pending') {
-      terminal.status = 'processing'
-      const newRecord = {
-        id: Date.now() + Math.random(),
-        time: new Date().toLocaleString(),
-        type: 'primary',
-        dispatchedTo: target.name,
-        description: `批量派发任务,${batchDispatchForm.value.targetType === 'construction' ? '请施工单位' : '请维护人员'}及时处理`
+  submitting.value = true
+  try {
+    let successCount = 0
+    let failCount = 0
+    for (const id of batchDispatchIds.value) {
+      const terminal = devices.value.find(t => t.id === id)
+      if (!terminal || terminal.workOrderSubmitted) continue
+      try {
+        await addManholeWorkOrder({
+          deviceId: terminal.equipmentId,
+          orderType: batchDispatchForm.value.orderType,
+          orderLevel: batchDispatchForm.value.orderLevel,
+          orderDesc: batchDispatchForm.value.orderDesc
+        })
+        terminal.workOrderSubmitted = true
+        successCount++
+      } catch {
+        failCount++
       }
-      if (!terminal.historyRecords) terminal.historyRecords = []
-      terminal.historyRecords.unshift(newRecord)
     }
-  })
-
-  ElMessage.success(`已批量派发 ${batchDispatchIds.value.length} 个任务至 ${target.name}`)
-  batchDispatchDialogVisible.value = false
-  clearSelection()
+    if (successCount > 0) {
+      ElMessage.success(`成功提交 ${successCount} 个工单${failCount > 0 ? `,${failCount}个失败` : ''}`)
+    } else {
+      ElMessage.error('批量提交工单全部失败')
+    }
+    batchDispatchDialogVisible.value = false
+    clearSelection()
+  } catch (error) {
+    const msg = error?.message || error?.msg || '未知错误'
+    ElMessage.error('批量提交失败: ' + msg)
+  } finally {
+    submitting.value = false
+  }
 }
 
-const onTargetTypeChange = () => {
-  dispatchForm.value.targetId = ''
-}
+// ==================== 生命周期 ====================
+onMounted(() => {
+  loadDevices()
+})
 </script>
 
 <style scoped>
@@ -539,7 +659,7 @@ const onTargetTypeChange = () => {
   align-items: center;
   flex-wrap: wrap;
   gap: 12px;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.04);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
 }
 
 .header-left {
@@ -574,7 +694,7 @@ const onTargetTypeChange = () => {
   padding: 12px 24px;
   text-align: center;
   min-width: 110px;
-  box-shadow: 0 1px 4px rgba(0,0,0,0.05);
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
 }
 
 .stat-card.critical .stat-value { color: #f56c6c; }
@@ -648,20 +768,34 @@ const onTargetTypeChange = () => {
 .terminal-header {
   display: flex;
   justify-content: space-between;
-  margin-bottom: 6px;
+  margin-bottom: 4px;
 }
 
 .terminal-code { font-weight: 600; font-size: 14px; }
+.terminal-name { font-size: 13px; color: #303133; margin-bottom: 4px; }
 .terminal-location { font-size: 12px; color: #6c757d; display: flex; align-items: center; gap: 4px; margin-bottom: 4px; }
 .terminal-desc { font-size: 12px; color: #f56c6c; margin-bottom: 4px; }
 .terminal-time { font-size: 11px; color: #aaa; }
 
+.terminal-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 6px;
+}
+
+.type-tag {
+  font-size: 11px;
+  color: #909399;
+}
+
 .detail-panel {
   flex: 1;
   background: white;
   border-radius: 16px;
   padding: 20px;
   overflow-y: auto;
+  max-height: calc(100vh - 160px);
 }
 
 .empty-panel {
@@ -682,22 +816,57 @@ const onTargetTypeChange = () => {
   align-items: center;
 }
 
-.map-container { margin-bottom: 20px; }
-.mock-map {
-  background: #e9f5e9;
-  border-radius: 12px;
-  height: 140px;
+.info-section { margin-bottom: 20px; }
+
+.monitor-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 12px;
+}
+
+.monitor-item {
+  background: #f5f7fa;
+  border-radius: 8px;
+  padding: 12px;
+  text-align: center;
+  border: 1px solid #e9ecef;
+}
+
+.monitor-item.alert {
+  background: #fef0f0;
+  border-color: #f56c6c;
+}
+
+.monitor-label { font-size: 12px; color: #909399; margin-bottom: 4px; }
+.monitor-value { font-size: 18px; font-weight: 600; color: #303133; }
+.monitor-item.alert .monitor-value { color: #f56c6c; }
+.monitor-extra { font-size: 11px; color: #aaa; margin-top: 2px; }
+
+.last-upload {
+  margin-top: 10px;
+  font-size: 12px;
+  color: #909399;
+}
+
+.location-section { margin-bottom: 20px; }
+
+.location-info {
   display: flex;
   align-items: center;
-  justify-content: center;
-  gap: 16px;
+  gap: 8px;
+  background: #f5f7fa;
+  border-radius: 8px;
+  padding: 16px;
+  font-size: 14px;
+  color: #303133;
 }
 
+.location-info .coords { color: #909399; font-size: 12px; }
+
 .record-content { font-size: 13px; }
-.record-images { display: flex; gap: 8px; margin-top: 8px; }
-.history-img { width: 60px; height: 50px; border-radius: 6px; object-fit: cover; }
+.record-row { margin-bottom: 4px; }
 
 .pagination-area { padding: 12px; text-align: center; border-top: 1px solid #eee; }
 
 .batch-list { max-height: 150px; overflow-y: auto; }
-</style>
+</style>

+ 2 - 1
vite.config.js

@@ -31,7 +31,8 @@ export default defineConfig(({ mode, command }) => {
       proxy: {
         '/dev-api': {
           // target: 'http://192.168.110.235:8300/pipe',
-          target: 'http://127.0.0.1:8300/pipe',
+          target: 'http://localhost:8301',
+          //target: 'http://127.0.0.1:8300/pipe',
           // target: 'http://111.23.174.45:8301',
           changeOrigin: true,
           rewrite: (p) => p.replace(/^\/dev-api/, '')

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů