Przeglądaj źródła

Merge remote-tracking branch 'origin/master'

Kazerin 1 tydzień temu
rodzic
commit
ba6783f203
31 zmienionych plików z 8208 dodań i 5251 usunięć
  1. 23 0
      src/api/drainage.js
  2. 139 7
      src/api/pipeNetwork/basic.js
  3. 165 0
      src/api/pipeNetwork/earlyWarning.js
  4. 21 3
      src/api/work/order.js
  5. 57 87
      src/components/ImageUpload/index.vue
  6. 239 0
      src/components/RepairImagePanel/index.vue
  7. 71 25
      src/views/subSystem/basic/BusinessWorkOrder.vue
  8. 329 0
      src/views/subSystem/basic/GasThreshold.vue
  9. 71 25
      src/views/subSystem/drainage/DrainageWorkOrder.vue
  10. 13 6
      src/views/subSystem/drainage/jcsj/sbyx.vue
  11. 1 1
      src/views/subSystem/drainage/jcsj/yjsdgl.vue
  12. 340 0
      src/views/subSystem/drainage/jcyj/DrainageThreshold.vue
  13. 138 43
      src/views/subSystem/manholeCover/alarm/alarmList.vue
  14. 157 94
      src/views/subSystem/manholeCover/alarm/alarmSanalysis.vue
  15. 47 16
      src/views/subSystem/manholeCover/alarm/index.vue
  16. 202 5
      src/views/subSystem/manholeCover/caseHandling/orderDisposal.vue
  17. 225 285
      src/views/subSystem/manholeCover/device/deviceApicture.vue
  18. 401 312
      src/views/subSystem/manholeCover/device/index.vue
  19. 72 318
      src/views/subSystem/manholeCover/device/statistics.vue
  20. 523 145
      src/views/subSystem/manholeCover/home/index.vue
  21. 371 419
      src/views/subSystem/manholeCover/mRealTimemonit/mRStateMonitoring.vue
  22. 173 96
      src/views/subSystem/manholeCover/mRealTimemonit/mRealApicture.vue
  23. 425 396
      src/views/subSystem/manholeCover/operation/faultMana.vue
  24. 389 187
      src/views/subSystem/manholeCover/operation/mOdataStatistics.vue
  25. 480 311
      src/views/subSystem/manholeCover/operation/operationTask.vue
  26. 523 576
      src/views/subSystem/pipeNetwork/pAlarmMonitor/PEqWarning.vue
  27. 790 687
      src/views/subSystem/pipeNetwork/pworkOrder/Pdayinspect.vue
  28. 914 577
      src/views/subSystem/pipeNetwork/pworkOrder/PworkList.vue
  29. 785 629
      src/views/subSystem/pipeNetwork/pworkOrder/Pworkdisposition.vue
  30. 122 0
      src/views/subSystem/pipeNetwork/pworkOrder/workOrderDict.js
  31. 2 1
      vite.config.js

+ 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 })
+}

+ 165 - 0
src/api/pipeNetwork/earlyWarning.js

@@ -0,0 +1,165 @@
+import request from '@/utils/request'
+
+/**
+ * 设备预警管理 API
+ * 数据源:early_warning 预警体系(/warning/*)
+ * 处置人分配复用工单接口:addManholeWorkOrder / operManholeWorkOrder(basic.js)
+ */
+
+// 预警综合搜索(名称/编号/类型/级别/状态/位置/发布人/权属单位/时间范围)
+export function getWarningSearchData(query) {
+  return request({
+    url: '/warning/search',
+    method: 'get',
+    params: query
+  })
+}
+
+// 待办预警列表(当前用户)
+export function getWarningTodoListData(query) {
+  return request({
+    url: '/warning/todo/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 预警完整详情(基本信息+GIS+处置历史+附件+归档+关联报警)
+export function getWarningFullDetailData(warningId) {
+  return request({
+    url: `/warning/detail/full/${warningId}`,
+    method: 'get'
+  })
+}
+
+// 发布预警(确认异常)
+export function publishWarningData(warningId) {
+  return request({
+    url: `/warning/publish/${warningId}`,
+    method: 'post'
+  })
+}
+
+// 解除预警(误报/工单完成)
+export function resolveWarningData(data) {
+  return request({
+    url: '/warning/resolve',
+    method: 'post',
+    params: data
+  })
+}
+
+// 预警升级
+export function upgradeWarningData(data) {
+  return request({
+    url: '/warning/upgrade',
+    method: 'post',
+    params: data
+  })
+}
+
+// 预警退回重办
+export function returnWarningData(data) {
+  return request({
+    url: '/warning/return',
+    method: 'post',
+    params: data
+  })
+}
+
+// 预警督办
+export function supervisionWarningData(data) {
+  return request({
+    url: '/warning/supervision',
+    method: 'post',
+    params: data
+  })
+}
+
+// 领导批示
+export function instructionWarningData(data) {
+  return request({
+    url: '/warning/instruction',
+    method: 'post',
+    params: data
+  })
+}
+
+// 处置历史记录
+export function getWarningDisposalHistoryData(warningId) {
+  return request({
+    url: `/warning/disposal/history/${warningId}`,
+    method: 'get'
+  })
+}
+
+// 附件列表
+export function getWarningAttachmentsData(warningId) {
+  return request({
+    url: `/warning/attachments/${warningId}`,
+    method: 'get'
+  })
+}
+
+// 流程可视化数据(路线图节点+边)
+export function getWarningDiagramData(warningId) {
+  return request({
+    url: `/warning/diagram/${warningId}`,
+    method: 'get'
+  })
+}
+
+// 仪表盘统计(总数/类型分布/级别分布/近7天趋势)
+export function getWarningDashboardStatisticsData() {
+  return request({
+    url: '/warning/dashboard/statistics',
+    method: 'get'
+  })
+}
+
+// 处置中预警列表
+export function getWarningProcessingListData(query) {
+  return request({
+    url: '/warning/processing/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 已完成预警列表
+export function getWarningCompletedListData(query) {
+  return request({
+    url: '/warning/completed/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 预警归档信息
+export function getWarningArchiveData(warningId) {
+  return request({
+    url: `/warning/archive/${warningId}`,
+    method: 'get'
+  })
+}
+
+// 修改预警(处理反馈时上传附件:warning JSON + files)
+export function updateWarningData(data) {
+  const formData = new FormData()
+  const warning = data.warning || {}
+  if (warning.warningId) {
+    formData.append('warning', JSON.stringify(warning))
+  } else {
+    const { warningId, ...rest } = warning
+    formData.append('warning', JSON.stringify(rest))
+  }
+  formData.append('alarmIds', JSON.stringify(data.alarmIds || []))
+  ;(data.files || []).forEach((file) => {
+    formData.append('files', file)
+  })
+  return request({
+    url: '/warning/update',
+    method: 'put',
+    data: formData
+  })
+}

+ 21 - 3
src/api/work/order.js

@@ -44,12 +44,21 @@ export function processWorkOrder(data) {
   })
 }
 
-/** 完成/提交验收 */
-export function completeWorkOrder(orderId, result) {
+/** 完成/提交验收,提交维修后图片 */
+export function completeWorkOrder(data) {
   return request({
     url: '/api/work-order/complete',
     method: 'put',
-    params: { orderId, result }
+    data
+  })
+}
+
+/** 新增工单(手动建单) */
+export function addWorkOrder(data) {
+  return request({
+    url: '/api/work-order/add',
+    method: 'post',
+    data
   })
 }
 
@@ -61,6 +70,15 @@ export function cancelWorkOrder(orderId) {
   })
 }
 
+/** 驳回工单(待派单/待接单 -> 已驳回) */
+export function rejectWorkOrder(data) {
+  return request({
+    url: '/api/work-order/reject',
+    method: 'put',
+    data
+  })
+}
+
 /** 验收通过/结案 */
 export function closeWorkOrder(data) {
   return request({

+ 57 - 87
src/components/ImageUpload/index.vue

@@ -19,7 +19,6 @@
     >
       <el-icon class="avatar-uploader-icon"><plus /></el-icon>
     </el-upload>
-    <!-- 上传提示 -->
     <div class="el-upload__tip" v-if="showTip">
       请上传
       <template v-if="fileSize">
@@ -31,16 +30,8 @@
       的文件
     </div>
 
-    <el-dialog
-      v-model="dialogVisible"
-      title="预览"
-      width="800px"
-      append-to-body
-    >
-      <img
-        :src="dialogImageUrl"
-        style="display: block; max-width: 100%; margin: 0 auto"
-      />
+    <el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
+      <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
     </el-dialog>
   </div>
 </template>
@@ -50,64 +41,51 @@ import { getToken } from "@/utils/auth";
 
 const props = defineProps({
   modelValue: [String, Object, Array],
-  // 图片数量限制
-  limit: {
-    type: Number,
-    default: 5,
-  },
-  // 大小限制(MB)
-  fileSize: {
-    type: Number,
-    default: 5,
-  },
-  // 文件类型, 例如['png', 'jpg', 'jpeg']
-  fileType: {
-    type: Array,
-    default: () => ["png", "jpg", "jpeg"],
-  },
-  // 是否显示提示
-  isShowTip: {
-    type: Boolean,
-    default: true
-  },
+  limit: { type: Number, default: 5 },
+  fileSize: { type: Number, default: 5 },
+  fileType: { type: Array, default: () => ["png", "jpg", "jpeg"] },
+  isShowTip: { type: Boolean, default: true },
+  uploadUrl: { type: String, default: "" }
 });
 
 const { proxy } = getCurrentInstance();
-const emit = defineEmits();
+const emit = defineEmits(["update:modelValue"]);
 const number = ref(0);
 const uploadList = ref([]);
 const dialogImageUrl = ref("");
 const dialogVisible = ref(false);
 const baseUrl = import.meta.env.VITE_APP_BASE_API;
-const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 上传的图片服务器地址
+const uploadImgUrl = ref(props.uploadUrl || baseUrl + "/common/upload");
 const headers = ref({ Authorization: "Bearer " + getToken() });
 const fileList = ref([]);
-const showTip = computed(
-  () => props.isShowTip && (props.fileType || props.fileSize)
-);
+const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
 
 watch(() => props.modelValue, val => {
-  if (val) {
-    // 首先将值转为数组
-    const list = Array.isArray(val) ? val : props.modelValue.split(",");
-    // 然后将数组转为对象数组
+  if (val && (Array.isArray(val) || typeof val === "object" || String(val).trim())) {
+    const list = Array.isArray(val)
+      ? val
+      : typeof val === "string"
+        ? val.split(",")
+        : [val];
     fileList.value = list.map(item => {
-      if (typeof item === "string") {
-        if (item.indexOf(baseUrl) === -1) {
-          item = { name: baseUrl + item, url: baseUrl + item };
-        } else {
-          item = { name: item, url: item };
-        }
-      }
-      return item;
-    });
+      const source = typeof item === "string" ? item : (item.url || item.fileName || item.name);
+      const url = normalizeImageUrl(source);
+      return typeof item === "object"
+        ? { ...item, name: item.name || source || url, url, status: item.status || "success" }
+        : { name: source || url, url, status: "success" };
+    }).filter(item => item.url);
   } else {
     fileList.value = [];
-    return [];
   }
-},{ deep: true, immediate: true });
+}, { deep: true, immediate: true });
+
+function normalizeImageUrl(value) {
+  const url = String(value || "").trim();
+  if (!url || /^(https?:|blob:|data:)/.test(url)) return url;
+  if (baseUrl && (url === baseUrl || url.startsWith(baseUrl + "/"))) return url;
+  return url.startsWith("/") ? baseUrl + url : baseUrl + "/" + url;
+}
 
-// 上传前loading加载
 function handleBeforeUpload(file) {
   let isImg = false;
   if (props.fileType.length) {
@@ -115,40 +93,37 @@ function handleBeforeUpload(file) {
     if (file.name.lastIndexOf(".") > -1) {
       fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
     }
-    isImg = props.fileType.some(type => {
-      if (file.type.indexOf(type) > -1) return true;
-      if (fileExtension && fileExtension.indexOf(type) > -1) return true;
-      return false;
-    });
+    isImg = props.fileType.some(type => file.type.indexOf(type) > -1 || fileExtension.indexOf(type) > -1);
   } else {
     isImg = file.type.indexOf("image") > -1;
   }
   if (!isImg) {
-    proxy.$modal.msgError(
-      `文件格式不正确, 请上传${props.fileType.join("/")}图片格式文件!`
-    );
+    proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`);
     return false;
   }
-  if (props.fileSize) {
-    const isLt = file.size / 1024 / 1024 < props.fileSize;
-    if (!isLt) {
-      proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
-      return false;
-    }
+  if (props.fileSize && file.size / 1024 / 1024 >= props.fileSize) {
+    proxy.$modal.msgError(`上传图片大小不能超过 ${props.fileSize} MB!`);
+    return false;
   }
   proxy.$modal.loading("正在上传图片,请稍候...");
   number.value++;
 }
 
-// 文件个数超出
 function handleExceed() {
   proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
 }
 
-// 上传成功回调
 function handleUploadSuccess(res, file) {
   if (res.code === 200) {
-    uploadList.value.push({ name: res.fileName, url: res.fileName });
+    const storedUrl = uploadImgUrl.value.includes("/minio/")
+      ? (res.url || res.fileName)
+      : (res.fileName || res.url);
+    uploadList.value.push({
+      name: res.originalFilename || storedUrl,
+      url: normalizeImageUrl(storedUrl),
+      status: "success",
+      uid: file.uid
+    });
     uploadedSuccessfully();
   } else {
     number.value--;
@@ -159,9 +134,10 @@ function handleUploadSuccess(res, file) {
   }
 }
 
-// 删除图片
 function handleDelete(file) {
-  const findex = fileList.value.map(f => f.name).indexOf(file.name);
+  const findex = fileList.value.findIndex(f =>
+    (file.uid && f.uid === file.uid) || f.url === file.url || f.name === file.name
+  );
   if (findex > -1 && uploadList.value.length === number.value) {
     fileList.value.splice(findex, 1);
     emit("update:modelValue", listToString(fileList.value));
@@ -169,7 +145,6 @@ function handleDelete(file) {
   }
 }
 
-// 上传结束处理
 function uploadedSuccessfully() {
   if (number.value > 0 && uploadList.value.length === number.value) {
     fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
@@ -180,34 +155,29 @@ function uploadedSuccessfully() {
   }
 }
 
-// 上传失败
 function handleUploadError() {
   proxy.$modal.msgError("上传图片失败");
   proxy.$modal.closeLoading();
 }
 
-// 预览
 function handlePictureCardPreview(file) {
   dialogImageUrl.value = file.url;
   dialogVisible.value = true;
 }
 
-// 对象转成指定字符串分隔
-function listToString(list, separator) {
-  let strs = "";
-  separator = separator || ",";
-  for (let i in list) {
-    if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) {
-      strs += list[i].url.replace(baseUrl, "") + separator;
-    }
-  }
-  return strs != "" ? strs.substr(0, strs.length - 1) : "";
+function listToString(list, separator = ",") {
+  return list
+    .filter(item => item.url && !item.url.startsWith("blob:"))
+    .map(item => {
+      const url = item.url;
+      return baseUrl && url.startsWith(baseUrl) ? url.slice(baseUrl.length) : url;
+    })
+    .join(separator);
 }
 </script>
 
 <style scoped lang="scss">
-// .el-upload--picture-card 控制加号部分
 :deep(.hide .el-upload--picture-card) {
-    display: none;
+  display: none;
 }
-</style>
+</style>

+ 239 - 0
src/components/RepairImagePanel/index.vue

@@ -0,0 +1,239 @@
+<template>
+  <section class="repair-image-panel">
+    <div class="repair-image-panel__header">
+      <div class="repair-image-panel__title">
+        <el-icon><Camera /></el-icon>
+        <span>维修前后图片</span>
+      </div>
+      <span class="repair-image-panel__total">共 {{ allImages.length }} 张</span>
+    </div>
+
+    <div class="repair-image-panel__body">
+      <div class="repair-image-column">
+        <div class="repair-image-column__header">
+          <span>维修前</span>
+          <el-tag size="small" type="info">{{ beforeImageList.length }} 张</el-tag>
+        </div>
+        <div v-if="beforeImageList.length" class="repair-image-list">
+          <div
+            v-for="(img, index) in beforeImageList"
+            :key="`before-${index}-${img}`"
+            class="repair-image-item"
+          >
+            <el-image
+              class="repair-image-media"
+              :src="img"
+              :preview-src-list="beforeImageList"
+              :initial-index="index"
+              fit="cover"
+              preview-teleported
+            >
+              <template #error>
+                <div class="repair-image-error">图片加载失败</div>
+              </template>
+            </el-image>
+            <span class="repair-image-item__index">{{ index + 1 }}</span>
+          </div>
+        </div>
+        <div v-else class="repair-image-empty">暂无维修前图片</div>
+      </div>
+
+      <div class="repair-image-column">
+        <div class="repair-image-column__header">
+          <span>维修后</span>
+          <el-tag size="small" type="success">{{ afterImageList.length }} 张</el-tag>
+        </div>
+        <div v-if="afterImageList.length" class="repair-image-list">
+          <div
+            v-for="(img, index) in afterImageList"
+            :key="`after-${index}-${img}`"
+            class="repair-image-item"
+          >
+            <el-image
+              class="repair-image-media"
+              :src="img"
+              :preview-src-list="afterImageList"
+              :initial-index="index"
+              fit="cover"
+              preview-teleported
+            >
+              <template #error>
+                <div class="repair-image-error">图片加载失败</div>
+              </template>
+            </el-image>
+            <span class="repair-image-item__index">{{ index + 1 }}</span>
+          </div>
+        </div>
+        <div v-else class="repair-image-empty">暂无维修后图片</div>
+      </div>
+    </div>
+  </section>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import { Camera } from '@element-plus/icons-vue'
+
+const props = defineProps({
+  beforeImages: {
+    type: [String, Array, Object],
+    default: ''
+  },
+  afterImages: {
+    type: [String, Array, Object],
+    default: ''
+  }
+})
+
+const baseUrl = import.meta.env.VITE_APP_BASE_API
+
+function normalizeImageUrl(value) {
+  const url = String(value || '').trim()
+  if (!url || /^(https?:|blob:|data:)/.test(url)) return url
+  if (baseUrl && (url === baseUrl || url.startsWith(`${baseUrl}/`))) return url
+  return url.startsWith('/') ? baseUrl + url : `${baseUrl}/${url}`
+}
+
+function toImageList(value) {
+  const values = Array.isArray(value)
+    ? value
+    : typeof value === 'string'
+      ? value.split(',')
+      : value
+        ? [value]
+        : []
+
+  return values
+    .map(item => typeof item === 'string' ? item : (item.url || item.fileName || item.name))
+    .map(normalizeImageUrl)
+    .filter(Boolean)
+}
+
+const beforeImageList = computed(() => toImageList(props.beforeImages))
+const afterImageList = computed(() => toImageList(props.afterImages))
+const allImages = computed(() => beforeImageList.value.concat(afterImageList.value))
+</script>
+
+<style scoped lang="scss">
+.repair-image-panel {
+  margin-top: 20px;
+  padding-top: 16px;
+  border-top: 1px solid #e9ecef;
+}
+
+.repair-image-panel__header,
+.repair-image-column__header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.repair-image-panel__header {
+  margin-bottom: 12px;
+}
+
+.repair-image-panel__title {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  color: #303133;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.repair-image-panel__total {
+  color: #909399;
+  font-size: 13px;
+}
+
+.repair-image-panel__body {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.repair-image-column {
+  min-width: 0;
+  padding: 14px;
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  background: #fafbfc;
+}
+
+.repair-image-column__header {
+  margin-bottom: 12px;
+  color: #606266;
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.repair-image-list {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
+  gap: 10px;
+}
+
+.repair-image-item {
+  position: relative;
+  width: 100%;
+  aspect-ratio: 4 / 3;
+  overflow: hidden;
+  border: 1px solid #dcdfe6;
+  border-radius: 6px;
+  background: #f2f6fc;
+  cursor: pointer;
+}
+
+.repair-image-item:hover :deep(.el-image__inner) {
+  transform: scale(1.04);
+}
+
+.repair-image-media {
+  width: 100%;
+  height: 100%;
+}
+
+.repair-image-media :deep(.el-image__inner) {
+  transition: transform 0.2s ease;
+}
+
+.repair-image-item__index {
+  position: absolute;
+  right: 5px;
+  bottom: 5px;
+  min-width: 20px;
+  padding: 2px 5px;
+  border-radius: 10px;
+  background: rgb(0 0 0 / 55%);
+  color: #fff;
+  font-size: 12px;
+  line-height: 16px;
+  text-align: center;
+}
+
+.repair-image-empty,
+.repair-image-error {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #a8abb2;
+  font-size: 13px;
+}
+
+.repair-image-empty {
+  min-height: 120px;
+  border: 1px dashed #dcdfe6;
+  border-radius: 6px;
+}
+
+.repair-image-error {
+  width: 100%;
+  height: 100%;
+}
+
+@media (max-width: 900px) {
+  .repair-image-panel__body {
+    grid-template-columns: 1fr;
+  }
+}
+</style>

+ 71 - 25
src/views/subSystem/basic/BusinessWorkOrder.vue

@@ -234,19 +234,7 @@
                   <el-descriptions-item label="处置结果">{{ selectedOrder.repairResult || '-' }}</el-descriptions-item>
                 </el-descriptions>
                 
-                <!-- 工单图片 -->
-                <div v-if="workImageList.length > 0" class="image-section">
-                  <h4 class="section-title">工单图片</h4>
-                  <div class="image-list">
-                    <image-preview 
-                      v-for="(img, index) in workImageList" 
-                      :key="index" 
-                      :src="img" 
-                      :width="100" 
-                      :height="100" 
-                    />
-                  </div>
-                </div>
+                <RepairImagePanel :before-images="selectedOrder.maintenanceBeforeImg" :after-images="selectedOrder.maintenanceAfterImg" />
                 
                 <!-- 工单操作 -->
                 <div class="action-section">
@@ -386,6 +374,7 @@
     
     <!-- 核查/处理弹窗 -->
     <el-dialog v-model="actionDialogVisible" :title="actionTitle" width="600px">
+      <ImageUpload v-if="actionTitle === '处理工单'" v-model="processBeforeImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
       <el-input v-model="actionFormData.solution" type="textarea" :rows="6" placeholder="请输入说明文字" />
       <template #footer>
         <el-button @click="actionDialogVisible = false">取消</el-button>
@@ -394,7 +383,8 @@
     </el-dialog>
     
     <!-- 完成/验收弹窗 -->
-    <el-dialog v-model="completeDialogVisible" title="提交验收" width="500px">
+    <el-dialog v-model="completeDialogVisible" title="提交验收" width="600px">
+      <ImageUpload v-model="maintenanceAfterImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
       <el-form label-width="80px">
         <el-form-item label="验收意见">
           <el-input v-model="completeForm.result" type="textarea" :rows="4" />
@@ -418,13 +408,27 @@
         <el-button type="primary" @click="submitClose">确认验收</el-button>
       </template>
     </el-dialog>
+
+    <!-- 驳回工单弹窗 -->
+    <el-dialog v-model="rejectDialogVisible" title="驳回工单" width="500px">
+      <el-form label-width="80px">
+        <el-form-item label="驳回原因" required>
+          <el-input v-model="rejectReason" type="textarea" :rows="4" placeholder="请输入驳回原因(驳回后工单状态为已驳回)" maxlength="300" show-word-limit />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="rejectDialogVisible = false">取消</el-button>
+        <el-button type="danger" :loading="rejecting" @click="submitReject">确认驳回</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="BusinessWorkOrder">
 import { ElMessage } from 'element-plus'
-import ImagePreview from '@/components/ImagePreview'
-import { listWorkOrder, getWorkOrderDetail, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder } from '@/api/work/order'
+import ImageUpload from '@/components/ImageUpload'
+import RepairImagePanel from '@/components/RepairImagePanel'
+import { listWorkOrder, getWorkOrderDetail, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder, rejectWorkOrder } from '@/api/work/order'
 import { listDept } from '@/api/system/dept'
 import { listUser } from '@/api/system/user'
 import { handleTree } from '@/utils/ruoyi'
@@ -449,12 +453,6 @@ const selectedOrder = ref({})
 const activeTab = ref('info')
 const currentOrderLogs = ref([])
 
-// 工单图片列表(多个用逗号分隔)
-const workImageList = computed(() => {
-  if (!selectedOrder.value.workImage) return []
-  return selectedOrder.value.workImage.split(',').filter(img => img && img.trim())
-})
-
 // 派单相关
 const dispatchDialogVisible = ref(false)
 const dispatchForm = reactive({ handlerIds: [], remarks: '' })
@@ -606,6 +604,8 @@ async function submitDispatch() {
 const actionDialogVisible = ref(false)
 const actionTitle = ref('')
 const actionFormData = reactive({ solution: '' })
+const processBeforeImages = ref('')
+const imageUploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload'
 
 function handleVerify() {
   if (![2, 3].includes(selectedOrder.value.orderStatus)) {
@@ -614,6 +614,7 @@ function handleVerify() {
   }
   actionTitle.value = '核查工单'
   actionFormData.solution = ''
+  processBeforeImages.value = ''
   actionDialogVisible.value = true
 }
 
@@ -624,6 +625,7 @@ function handleProcess() {
   }
   actionTitle.value = '处理工单'
   actionFormData.solution = ''
+  processBeforeImages.value = ''
   actionDialogVisible.value = true
 }
 
@@ -638,6 +640,9 @@ async function submitAction() {
       handleResult: 'solved',
       solution: actionFormData.solution
     }
+    if (actionTitle.value === '处理工单' && processBeforeImages.value) {
+      data.maintenanceBeforeImg = processBeforeImages.value
+    }
     if (actionTitle.value === '核查工单') {
       await verifyWorkOrder(data)
     } else {
@@ -655,6 +660,7 @@ async function submitAction() {
 /** 完成/验收相关 */
 const completeDialogVisible = ref(false)
 const completeForm = reactive({ result: '' })
+const maintenanceAfterImages = ref('')
 
 function handleComplete() {
   if (![4, 5].includes(selectedOrder.value.orderStatus)) {
@@ -662,6 +668,7 @@ function handleComplete() {
     return
   }
   completeForm.result = ''
+  maintenanceAfterImages.value = ''
   completeDialogVisible.value = true
 }
 
@@ -671,7 +678,11 @@ async function submitComplete() {
     return
   }
   try {
-    await completeWorkOrder(selectedOrder.value.orderId, completeForm.result)
+    await completeWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      result: completeForm.result,
+      maintenanceAfterImg: maintenanceAfterImages.value
+    })
     ElMessage.success('提交验收成功')
     completeDialogVisible.value = false
     handleSearch()
@@ -681,13 +692,38 @@ async function submitComplete() {
 }
 
 /** 驳回相关 */
+const rejectDialogVisible = ref(false)
+const rejectReason = ref('')
+const rejecting = ref(false)
+
 function handleReject() {
   if (![1, 2].includes(selectedOrder.value.orderStatus)) {
     ElMessage.warning('当前工单不允许驳回')
     return
   }
-  ElMessage.info('驳回功能待实现')
-  // TODO: 实现驳回逻辑
+  rejectReason.value = ''
+  rejectDialogVisible.value = true
+}
+
+async function submitReject() {
+  if (!rejectReason.value.trim()) {
+    ElMessage.warning('请填写驳回原因')
+    return
+  }
+  rejecting.value = true
+  try {
+    await rejectWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      reason: rejectReason.value.trim()
+    })
+    ElMessage.success('驳回成功,工单状态已更新为已驳回')
+    rejectDialogVisible.value = false
+    handleSearch()
+  } catch (e) {
+    ElMessage.error('驳回失败:' + (e.msg || e.message || '请重试'))
+  } finally {
+    rejecting.value = false
+  }
 }
 
 /** 验收通过/结案相关 */
@@ -916,6 +952,16 @@ onMounted(() => {
     margin-top: 24px;
     padding-top: 24px;
     border-top: 1px solid #e4e7ed;
+
+    .image-group {
+      margin-bottom: 16px;
+    }
+
+    .image-group-title {
+      margin-bottom: 8px;
+      color: #606266;
+      font-weight: 600;
+    }
     
     .image-list {
       display: flex;

+ 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>

+ 71 - 25
src/views/subSystem/drainage/DrainageWorkOrder.vue

@@ -232,19 +232,7 @@
                   <el-descriptions-item label="处置结果">{{ selectedOrder.repairResult || '-' }}</el-descriptions-item>
                 </el-descriptions>
                 
-                <!-- 工单图片 -->
-                <div v-if="workImageList.length > 0" class="image-section">
-                  <h4 class="section-title">工单图片</h4>
-                  <div class="image-list">
-                    <image-preview 
-                      v-for="(img, index) in workImageList" 
-                      :key="index" 
-                      :src="img" 
-                      :width="100" 
-                      :height="100" 
-                    />
-                  </div>
-                </div>
+                <RepairImagePanel :before-images="selectedOrder.maintenanceBeforeImg" :after-images="selectedOrder.maintenanceAfterImg" />
                 
                 <!-- 工单操作 -->
                 <div class="action-section">
@@ -384,6 +372,7 @@
     
     <!-- 核查/处理弹窗 -->
     <el-dialog v-model="actionDialogVisible" :title="actionTitle" width="600px">
+      <ImageUpload v-if="actionTitle === '处理工单'" v-model="processBeforeImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
       <el-input v-model="actionFormData.solution" type="textarea" :rows="6" placeholder="请输入说明文字" />
       <template #footer>
         <el-button @click="actionDialogVisible = false">取消</el-button>
@@ -392,7 +381,8 @@
     </el-dialog>
     
     <!-- 完成/验收弹窗 -->
-    <el-dialog v-model="completeDialogVisible" title="提交验收" width="500px">
+    <el-dialog v-model="completeDialogVisible" title="提交验收" width="600px">
+      <ImageUpload v-model="maintenanceAfterImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
       <el-form label-width="80px">
         <el-form-item label="验收意见">
           <el-input v-model="completeForm.result" type="textarea" :rows="4" />
@@ -416,13 +406,27 @@
         <el-button type="primary" @click="submitClose">确认验收</el-button>
       </template>
     </el-dialog>
+
+    <!-- 驳回工单弹窗 -->
+    <el-dialog v-model="rejectDialogVisible" title="驳回工单" width="500px">
+      <el-form label-width="80px">
+        <el-form-item label="驳回原因" required>
+          <el-input v-model="rejectReason" type="textarea" :rows="4" placeholder="请输入驳回原因(驳回后工单状态为已驳回)" maxlength="300" show-word-limit />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="rejectDialogVisible = false">取消</el-button>
+        <el-button type="danger" :loading="rejecting" @click="submitReject">确认驳回</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="DrainageWorkOrder">
 import { ElMessage } from 'element-plus'
-import ImagePreview from '@/components/ImagePreview/index.vue'
-import { listWorkOrder, getWorkOrderDetail, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder } from '@/api/work/order.js'
+import ImageUpload from '@/components/ImageUpload/index.vue'
+import RepairImagePanel from '@/components/RepairImagePanel/index.vue'
+import { listWorkOrder, getWorkOrderDetail, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder, rejectWorkOrder } from '@/api/work/order.js'
 import { listDept } from '@/api/system/dept.js'
 import { listUser } from '@/api/system/user.js'
 import { handleTree } from '@/utils/ruoyi.js'
@@ -447,12 +451,6 @@ const selectedOrder = ref({})
 const activeTab = ref('info')
 const currentOrderLogs = ref([])
 
-// 工单图片列表(多个用逗号分隔)
-const workImageList = computed(() => {
-  if (!selectedOrder.value.workImage) return []
-  return selectedOrder.value.workImage.split(',').filter(img => img && img.trim())
-})
-
 // 派单相关
 const dispatchDialogVisible = ref(false)
 const dispatchForm = reactive({ handlerIds: [], remarks: '' })
@@ -604,6 +602,8 @@ async function submitDispatch() {
 const actionDialogVisible = ref(false)
 const actionTitle = ref('')
 const actionFormData = reactive({ solution: '' })
+const processBeforeImages = ref('')
+const imageUploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload'
 
 function handleVerify() {
   if (![2, 3].includes(selectedOrder.value.orderStatus)) {
@@ -612,6 +612,7 @@ function handleVerify() {
   }
   actionTitle.value = '核查工单'
   actionFormData.solution = ''
+  processBeforeImages.value = ''
   actionDialogVisible.value = true
 }
 
@@ -622,6 +623,7 @@ function handleProcess() {
   }
   actionTitle.value = '处理工单'
   actionFormData.solution = ''
+  processBeforeImages.value = ''
   actionDialogVisible.value = true
 }
 
@@ -636,6 +638,9 @@ async function submitAction() {
       handleResult: 'solved',
       solution: actionFormData.solution
     }
+    if (actionTitle.value === '处理工单' && processBeforeImages.value) {
+      data.maintenanceBeforeImg = processBeforeImages.value
+    }
     if (actionTitle.value === '核查工单') {
       await verifyWorkOrder(data)
     } else {
@@ -652,6 +657,7 @@ async function submitAction() {
 /** 完成/验收相关 */
 const completeDialogVisible = ref(false)
 const completeForm = reactive({ result: '' })
+const maintenanceAfterImages = ref('')
 
 function handleComplete() {
   if (![4, 5].includes(selectedOrder.value.orderStatus)) {
@@ -659,6 +665,7 @@ function handleComplete() {
     return
   }
   completeForm.result = ''
+  maintenanceAfterImages.value = ''
   completeDialogVisible.value = true
 }
 
@@ -668,7 +675,11 @@ async function submitComplete() {
     return
   }
   try {
-    await completeWorkOrder(selectedOrder.value.orderId, completeForm.result)
+    await completeWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      result: completeForm.result,
+      maintenanceAfterImg: maintenanceAfterImages.value
+    })
     ElMessage.success('提交验收成功')
     completeDialogVisible.value = false
     handleSearch()
@@ -678,13 +689,38 @@ async function submitComplete() {
 }
 
 /** 驳回相关 */
+const rejectDialogVisible = ref(false)
+const rejectReason = ref('')
+const rejecting = ref(false)
+
 function handleReject() {
   if (![1, 2].includes(selectedOrder.value.orderStatus)) {
     ElMessage.warning('当前工单不允许驳回')
     return
   }
-  ElMessage.info('驳回功能待实现')
-  // TODO: 实现驳回逻辑
+  rejectReason.value = ''
+  rejectDialogVisible.value = true
+}
+
+async function submitReject() {
+  if (!rejectReason.value.trim()) {
+    ElMessage.warning('请填写驳回原因')
+    return
+  }
+  rejecting.value = true
+  try {
+    await rejectWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      reason: rejectReason.value.trim()
+    })
+    ElMessage.success('驳回成功,工单状态已更新为已驳回')
+    rejectDialogVisible.value = false
+    handleSearch()
+  } catch (e) {
+    ElMessage.error('驳回失败:' + (e.msg || e.message || '请重试'))
+  } finally {
+    rejecting.value = false
+  }
 }
 
 /** 验收通过/结案相关 */
@@ -913,6 +949,16 @@ onMounted(() => {
     margin-top: 24px;
     padding-top: 24px;
     border-top: 1px solid #e4e7ed;
+
+    .image-group {
+      margin-bottom: 16px;
+    }
+
+    .image-group-title {
+      margin-bottom: 8px;
+      color: #606266;
+      font-weight: 600;
+    }
     
     .image-list {
       display: flex;

+ 13 - 6
src/views/subSystem/drainage/jcsj/sbyx.vue

@@ -634,20 +634,27 @@ function handleRepairSubmit() {
       return
     }
     const orderLevel = urgencyToOrderLevel[repairForm.value.urgency]
-    try {
-      // 批量派单时为每个设备分别创建工单
-      for (const row of rows) {
+    // 批量派单时为每个设备分别创建工单(单个失败不影响其他设备,后端校验:同一设备仅允许一个进行中工单)
+    const failList = []
+    for (const row of rows) {
+      try {
         await addWorkOrder({
           deviceId: String(row.equipmentId),
           orderType: 1,
           orderLevel,
           orderDesc: repairForm.value.faultDesc
         })
+      } catch (error) {
+        failList.push((row.equipmentName || row.equipmentCode || row.equipmentId) + ':' + (error?.message || error?.msg || '创建失败'))
       }
-      ElMessage.success('维修派单成功')
+    }
+    if (failList.length === 0) {
+      ElMessage.success('维修派单成功,共 ' + rows.length + ' 个工单')
       repairDialogVisible.value = false
-    } catch (error) {
-      console.error('维修派单失败', error)
+    } else if (failList.length === rows.length) {
+      ElMessage.error('维修派单失败:' + failList[0])
+    } else {
+      ElMessage.warning('部分设备派单失败(' + failList.length + '/' + rows.length + '):' + failList.join(';'))
     }
   })
 }

+ 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>

+ 202 - 5
src/views/subSystem/manholeCover/caseHandling/orderDisposal.vue

@@ -103,6 +103,8 @@
               <el-descriptions-item label="处置结果" :span="2">{{ selectedOrder.repairResult || '—' }}</el-descriptions-item>
             </el-descriptions>
 
+            <RepairImagePanel :before-images="selectedOrder.maintenanceBeforeImg" :after-images="selectedOrder.maintenanceAfterImg" />
+
             <div class="mt-5 border-t border-[#e9ecef] pt-3">
               <div class="mb-3 mt-4 flex items-center gap-2 text-[16px] font-600">
                 <span><el-icon><Timer /></el-icon> 工单操作</span>
@@ -134,8 +136,24 @@
                 :timestamp="formatDateTime(log.operTime)"
                 :type="getLogType(log.operType)"
               >
-                {{ log.operDesc }}
-              <span v-if="log.operUserName" class="ml-3 text-[12px] text-[#909399]">操作人: {{ log.operUserName }}</span>
+                <div>
+                  <span>{{ log.operDesc }}</span>
+                  <span v-if="log.operUserName" class="ml-3 text-[12px] text-[#909399]">操作人:{{ log.operUserName }}</span>
+                  <!-- 显示处理图片(如果有的话) -->
+                  <div v-if="log.maintenanceImages && log.maintenanceImages.length" class="mt-2 flex gap-2">
+                    <div
+                      v-for="(img, idx) in log.maintenanceImages"
+                      :key="idx"
+                      class="cursor-pointer rounded border p-1 hover:shadow-md"
+                      @click="() => previewImage(img.url)"
+                    >
+                      <img :src="img.url" alt="处理图片" class="h-[60px] w-[80px] object-cover" />
+                      <div class="text-center text-xs text-[#909399]">
+                        {{ img.type === 'before' ? '⚠️ 处理前' : img.type === 'after' ? '✅ 处理后' : '🖼️ 图片' }}
+                      </div>
+                    </div>
+                  </div>
+                </div>
               </el-timeline-item>
             </el-timeline>
             <el-empty v-else description="暂无工单日志" :image-size="80" />
@@ -148,6 +166,22 @@
       </div>
     </div>
 
+    <el-dialog v-model="beforeImageDialogVisible" title="开始维修" width="600px">
+      <ImageUpload v-model="maintenanceBeforeImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
+      <template #footer>
+        <el-button @click="beforeImageDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitStartRepair">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <el-dialog v-model="afterImageDialogVisible" title="提交验收" width="600px">
+      <ImageUpload v-model="maintenanceAfterImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
+      <template #footer>
+        <el-button @click="afterImageDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitVerify">提交</el-button>
+      </template>
+    </el-dialog>
+
     <el-dialog v-model="delayDialogVisible" title="申请延期" width="450px">
       <el-form label-width="88px">
         <el-form-item label="截止时间">
@@ -216,13 +250,20 @@
         <el-button type="primary" @click="confirmDispatchUser">确认选择</el-button>
       </template>
     </el-dialog>
+
+    <!-- 图片预览弹窗 -->
+    <el-dialog v-model="imagePreviewVisible" title="图片预览" width="600px" append-to-body>
+      <img :src="currentImagePreviewUrl" alt="工单图片" class="max-w-full" style="border-radius: 4px;" />
+    </el-dialog>
   </div>
 </template>
 
 <script setup>
-import {computed, nextTick, onMounted, ref, watch} from 'vue'
+import{computed, nextTick, onMounted, ref, watch} from 'vue'
 import {Document, Search, Tickets, Timer} from '@element-plus/icons-vue'
 import {ElMessage} from 'element-plus'
+import ImageUpload from '@/components/ImageUpload/index.vue'
+import RepairImagePanel from '@/components/RepairImagePanel/index.vue'
 import {getManholeWorkOrderDetail, getManholeWorkOrderPage, operManholeWorkOrder} from '@/api/pipeNetwork/basic'
 import {deptTreeSelect, listUser} from '@/api/system/user'
 
@@ -242,6 +283,11 @@ const receiveUserDisplay = ref('')
 const delayDialogVisible = ref(false)
 const delayDeadline = ref('')
 const delayReason = ref('')
+const beforeImageDialogVisible = ref(false)
+const afterImageDialogVisible = ref(false)
+const maintenanceBeforeImages = ref('')
+const maintenanceAfterImages = ref('')
+const imageUploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload'
 const dispatchDialogVisible = ref(false)
 const dispatchUserLoading = ref(false)
 const deptOptions = ref([])
@@ -251,11 +297,24 @@ const dispatchUsers = ref([])
 const dispatchSelectedUser = ref(null)
 const deptTreeRef = ref(null)
 
+// 图片预览相关
+const imagePreviewVisible = ref(false)
+const currentImagePreviewUrl = ref('')
+const baseUrl = import.meta.env.VITE_APP_BASE_API
+
 function formatDateTime(value) {
   if (!value) return '—'
   return String(value).replace('T', ' ')
 }
 
+const maintenanceBeforeImageList = computed(() => splitImageList(selectedOrder.value?.maintenanceBeforeImg))
+const maintenanceAfterImageList = computed(() => splitImageList(selectedOrder.value?.maintenanceAfterImg))
+
+function previewImage(url) {
+  currentImagePreviewUrl.value = url
+  imagePreviewVisible.value = true
+}
+
 function getOrderLevelMeta(level) {
   const map = {
     1: { text: '紧急', tag: 'danger' },
@@ -581,8 +640,27 @@ async function acceptOrder() {
 }
 
 async function startRepair() {
+  maintenanceBeforeImages.value = ''
+  beforeImageDialogVisible.value = true
+}
+
+function normalizeImageUrl(value) {
+  const url = String(value || '').trim()
+  if (!url || /^(https?:|blob:|data:)/.test(url)) return url
+  if (baseUrl && (url === baseUrl || url.startsWith(`${baseUrl}/`))) return url
+  return url.startsWith('/') ? baseUrl + url : `${baseUrl}/${url}`
+}
+
+function splitImageList(value) {
+  return value
+    ? String(value).split(',').map(normalizeImageUrl).filter(Boolean)
+    : []
+}
+
+async function submitStartRepair() {
   try {
-    await operateCurrent(4, '开始维修')
+    await operateCurrent(4, '开始维修', { maintenanceBeforeImg: maintenanceBeforeImages.value })
+    beforeImageDialogVisible.value = false
     ElMessage.success('已开始维修')
   } catch (error) {
     ElMessage.error(error?.message || '开始维修失败')
@@ -590,8 +668,14 @@ async function startRepair() {
 }
 
 async function submitVerify() {
+  if (!afterImageDialogVisible.value) {
+    maintenanceAfterImages.value = ''
+    afterImageDialogVisible.value = true
+    return
+  }
   try {
-    await operateCurrent(5, '提交验收')
+    await operateCurrent(5, '提交验收', { maintenanceAfterImg: maintenanceAfterImages.value })
+    afterImageDialogVisible.value = false
     ElMessage.success('已提交验收')
   } catch (error) {
     ElMessage.error(error?.message || '提交验收失败')
@@ -670,3 +754,116 @@ onMounted(() => {
   loadOrders()
 })
 </script>
+
+<style scoped lang="scss">
+.repair-image-panel {
+  margin-top: 20px;
+  padding-top: 16px;
+  border-top: 1px solid #e9ecef;
+}
+
+.repair-image-panel__header,
+.repair-image-column__header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.repair-image-panel__header {
+  margin-bottom: 12px;
+}
+
+.repair-image-panel__title {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  color: #303133;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.repair-image-panel__total {
+  color: #909399;
+  font-size: 13px;
+}
+
+.repair-image-panel__body {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.repair-image-column {
+  min-width: 0;
+  padding: 14px;
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  background: #fafbfc;
+}
+
+.repair-image-column__header {
+  margin-bottom: 12px;
+  color: #606266;
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.repair-image-list {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
+  gap: 10px;
+}
+
+.repair-image-item {
+  position: relative;
+  aspect-ratio: 4 / 3;
+  overflow: hidden;
+  border: 1px solid #dcdfe6;
+  border-radius: 6px;
+  background: #f2f6fc;
+  cursor: pointer;
+}
+
+.repair-image-item:hover img {
+  transform: scale(1.04);
+}
+
+.repair-image-item img {
+  display: block;
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  transition: transform 0.2s ease;
+}
+
+.repair-image-item__index {
+  position: absolute;
+  right: 5px;
+  bottom: 5px;
+  min-width: 20px;
+  padding: 2px 5px;
+  border-radius: 10px;
+  background: rgb(0 0 0 / 55%);
+  color: #fff;
+  font-size: 12px;
+  line-height: 16px;
+  text-align: center;
+}
+
+.repair-image-empty {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 120px;
+  border: 1px dashed #dcdfe6;
+  border-radius: 6px;
+  color: #a8abb2;
+  font-size: 13px;
+}
+
+@media (max-width: 900px) {
+  .repair-image-panel__body {
+    grid-template-columns: 1fr;
+  }
+}
+</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>

Plik diff jest za duży
+ 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>

+ 523 - 576
src/views/subSystem/pipeNetwork/pAlarmMonitor/PEqWarning.vue

@@ -1,636 +1,583 @@
-<!--设备预警-->
-<template>
-  <div class="prototype-container">
-    <!-- 头部 -->
-    <div class="header">
-      <div class="logo">
-        <h2>⚠️ 设备预警与处理系统</h2>
-        <p>普通预警 | 特殊预警 | 预警确认 | 预警处理 | 物联感知一张图</p>
-      </div>
-      <div class="header-stats">
-        <div class="stat-card">
-          <span class="stat-value">{{ unconfirmedCount }}</span>
-          <span class="stat-label">待确认</span>
-        </div>
-        <div class="stat-card warning">
-          <span class="stat-value">{{ processingCount }}</span>
-          <span class="stat-label">处理中</span>
-        </div>
-        <div class="stat-card success">
-          <span class="stat-value">{{ todayCount }}</span>
-          <span class="stat-label">今日新增</span>
-        </div>
-      </div>
-    </div>
-
-    <div class="main-layout">
-      <!-- 左侧:物联感知一张图(特殊预警地图展示) -->
-      <div class="left-panel">
-        <div class="card">
-          <div class="card-header">
-            🗺️ 物联感知一张图
-            <span class="live-badge">● 实时预警</span>
-          </div>
-          <div class="card-body">
-            <div class="iot-map" id="iotMap">
-              <div class="map-container">
-                <!-- 模拟管网地图 -->
-                <svg class="map-svg" viewBox="0 0 600 400">
-                  <!-- 管网线路 -->
-                  <path d="M50,200 L150,200 L250,150 L350,150 L450,200 L550,200" stroke="#7f8c8d" stroke-width="3" fill="none" stroke-dasharray="6,3"/>
-                  <path d="M150,200 L150,300 L250,350 L350,350 L450,300 L450,200" stroke="#7f8c8d" stroke-width="3" fill="none" stroke-dasharray="6,3"/>
-                  <path d="M250,150 L250,80 L350,80 L350,150" stroke="#7f8c8d" stroke-width="3" fill="none" stroke-dasharray="6,3"/>
-
-                  <!-- 设备节点 -->
-                  <circle v-for="point in mapPoints" :key="point.id" :cx="point.x" :cy="point.y" r="12"
-                          :fill="getMapPointColor(point.status)" stroke="white" stroke-width="2"
-                          @click="showDeviceOnMap(point)" style="cursor:pointer;">
-                    <title>{{ point.name }}</title>
-                  </circle>
-                  <text v-for="point in mapPoints" :key="'t'+point.id" :x="point.x-10" :y="point.y-12" fill="white" font-size="8">{{ point.name }}</text>
-
-                  <!-- 特殊预警闪烁点 -->
-                  <circle v-for="alert in specialAlertsOnMap" :key="'a'+alert.id" :cx="alert.x" :cy="alert.y" r="18"
-                          fill="none" stroke="#e74c3c" stroke-width="2" class="pulse-ring">
-                    <animate attributeName="r" values="12;22;12" dur="1.5s" repeatCount="indefinite"/>
-                    <animate attributeName="opacity" values="1;0;1" dur="1.5s" repeatCount="indefinite"/>
-                  </circle>
-                </svg>
-
-                <!-- 图例 -->
-                <div class="map-legend">
-                  <div><span class="legend-dot normal"></span> 正常设备</div>
-                  <div><span class="legend-dot offline"></span> 离线/普通预警</div>
-                  <div><span class="legend-dot special"></span> 特殊预警</div>
-                  <div><span class="legend-dot processing"></span> 处理中</div>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <!-- 预警统计图表 -->
-        <div class="card">
-          <div class="card-header">📊 预警趋势统计</div>
-          <div class="card-body">
-            <div class="trend-chart">
-              <div v-for="(count, idx) in weeklyStats" :key="idx" class="trend-bar">
-                <div class="bar" :style="{ height: (count / maxWeeklyCount) * 60 + 'px' }"></div>
-                <div class="bar-label">{{ ['一', '二', '三', '四', '五', '六', '日'][idx] }}</div>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 右侧:预警列表与管理 -->
-      <div class="right-panel">
-        <!-- 查询筛选栏 -->
-        <div class="card">
-          <div class="card-header">
-            🔍 预警查询
-            <div class="filter-group">
-              <input type="date" v-model="queryParams.startDate" class="filter-input" placeholder="开始日期">
-              <span class="filter-sep">至</span>
-              <input type="date" v-model="queryParams.endDate" class="filter-input" placeholder="结束日期">
-              <select v-model="queryParams.status" class="filter-select">
-                <option value="">全部状态</option>
-                <option value="unconfirmed">待确认</option>
-                <option value="confirmed">已确认</option>
-                <option value="processing">处理中</option>
-                <option value="resolved">已清除</option>
-                <option value="misreport">误报</option>
-              </select>
-              <select v-model="queryParams.type" class="filter-select">
-                <option value="">全部类型</option>
-                <option value="normal">普通预警</option>
-                <option value="special">特殊预警</option>
-              </select>
-              <button @click="searchAlerts" class="btn-primary btn-sm">查询</button>
-              <button @click="resetQuery" class="btn-outline btn-sm">重置</button>
-            </div>
-          </div>
-        </div>
-
-        <!-- 预警列表(普通+特殊) -->
-        <div class="card">
-          <div class="card-header">
-            📋 设备预警列表
-            <span class="list-count">共 {{ filteredAlerts.length }} 条</span>
-          </div>
-          <div class="card-body">
-            <div class="alert-table">
-              <table class="data-table">
-                <thead>
-                <tr><th>预警时间</th><th>设备名称</th><th>设备位置</th><th>预警类型</th><th>预警内容</th><th>状态</th><th>操作</th>
-                </tr>
-                </thead>
-                <tbody>
-                <tr v-for="alert in paginatedAlerts" :key="alert.id">
-                  <td class="time-cell">{{ alert.time }}</td>
-                  <td>{{ alert.deviceName }}</td>
-                  <td>{{ alert.location }}</td>
-                  <td>
-                      <span class="alert-type" :class="alert.type === 'normal' ? 'type-normal' : 'type-special'">
-                        {{ alert.type === 'normal' ? '普通预警' : '特殊预警' }}
-                      </span>
-                  </td>
-                  <td class="content-cell">{{ alert.content }}</td>
-                  <td>
-                      <span class="status-badge" :class="getStatusClass(alert.status)">
-                        {{ getStatusText(alert.status) }}
-                      </span>
-                  </td>
-                  <td class="action-cell">
-                    <button v-if="alert.status === 'unconfirmed'" @click="confirmAlert(alert)" class="btn-confirm">确认</button>
-                    <button v-if="alert.status === 'unconfirmed'" @click="markMisreport(alert)" class="btn-misreport">误报</button>
-                    <button v-if="alert.status === 'confirmed'" @click="assignHandler(alert)" class="btn-assign">分配处理</button>
-                    <button v-if="alert.status === 'processing'" @click="processAlert(alert)" class="btn-process">处理反馈</button>
-                    <button v-if="alert.status === 'processing'" @click="viewDetail(alert)" class="btn-view">查看</button>
-                    <span v-if="alert.status === 'resolved' || alert.status === 'misreport'" class="completed-badge">✓ 已完成</span>
-                  </td>
-                </tr>
-                <tr v-if="paginatedAlerts.length === 0">
-                  <td colspan="7" class="empty-row">暂无预警数据</td>
-                </tr>
-                </tbody>
-              </table>
-            </div>
+<!--预警管理页面(RuoYi Element Plus 标准)-->
+<!--数据源:early_warning 体系 /warning/*接口 -->
+<!--确认异常后只建工单不派单,由工单系统处理后续流程-->
 
-            <!-- 分页 -->
-            <div class="pagination" v-if="totalPages > 1">
-              <button v-for="page in totalPages" :key="page" class="page-btn" :class="{ active: currentPage === page }"
-                      @click="currentPage = page">{{ page }}</button>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- 预警确认模态框 -->
-    <div v-if="confirmModalVisible" class="modal-overlay" @click.self="closeConfirmModal">
-      <div class="modal-content">
-        <h3>⚠️ 预警信息确认</h3>
-        <div class="confirm-info">
-          <div class="info-row"><span class="label">设备名称:</span>{{ currentAlert?.deviceName }}</div>
-          <div class="info-row"><span class="label">预警时间:</span>{{ currentAlert?.time }}</div>
-          <div class="info-row"><span class="label">预警内容:</span>{{ currentAlert?.content }}</div>
-        </div>
-        <div class="form-group">
-          <label class="form-label">确认类型</label>
-          <select v-model="confirmType" class="form-select">
-            <option value="device">设备异常预警(需处理)</option>
-            <option value="misreport">误报(网络等因素)</option>
-          </select>
-        </div>
-        <div class="form-group" v-if="confirmType === 'device'">
-          <label class="form-label">分配处理人员</label>
-          <select v-model="assignedPerson" class="form-select">
-            <option value="张工">张工(设备技术组)</option>
-            <option value="李工">李工(运维组)</option>
-            <option value="王工">王工(抢修组)</option>
-          </select>
+<template>
+  <div class="warning-container">
+    <!-- ========= 统计卡片区域 ========== -->
+    <el-row :gutter="16" class="stat-row">
+      <el-col :span="4">
+        <div class="stat-item">
+          <div class="stat-title">总预警</div>
+          <div class="stat-value" :class="stats.total > 0 ? 'primary' : ''">{{ stats.total }}</div>
         </div>
-        <div class="form-actions">
-          <button @click="closeConfirmModal" class="btn-outline">取消</button>
-          <button @click="submitConfirm" class="btn-primary">确认提交</button>
+      </el-col>
+      <el-col :span="4">
+        <div class="stat-item">
+          <div class="stat-title">待确认</div>
+          <div class="stat-value warning" :class="stats.unconfirmed > 0 ? 'highlight' : ''">{{ stats.unconfirmed }}</div>
         </div>
-      </div>
-    </div>
-
-    <!-- 预警处理模态框 -->
-    <div v-if="processModalVisible" class="modal-overlay" @click.self="closeProcessModal">
-      <div class="modal-content" style="width:500px;">
-        <h3>🔧 预警处理反馈</h3>
-        <div class="process-info">
-          <div class="info-row"><span class="label">设备:</span>{{ currentAlert?.deviceName }}</div>
-          <div class="info-row"><span class="label">预警内容:</span>{{ currentAlert?.content }}</div>
+      </el-col>
+      <el-col :span="4">
+        <div class="stat-item">
+          <div class="stat-title">处理中</div>
+          <div class="stat-value info" :class="stats.processing > 0 ? 'highlight' : ''">{{ stats.processing }}</div>
         </div>
-        <div class="form-group">
-          <label class="form-label">处理结果</label>
-          <textarea v-model="processResult" class="form-textarea" rows="3" placeholder="请描述处理过程及结果..."></textarea>
+      </el-col>
+      <el-col :span="4">
+        <div class="stat-item">
+          <div class="stat-title">已处置</div>
+          <div class="stat-value success" :class="stats.handled > 0 ? 'highlight' : ''">{{ stats.handled }}</div>
         </div>
-        <div class="form-group">
-          <label class="form-label">附件上传</label>
-          <div class="upload-area">
-            <label class="upload-btn">
-              📎 选择文件
-              <input type="file" @change="handleFileUpload" accept="image/*,.pdf" hidden multiple>
-            </label>
-            <div class="file-list">
-              <span v-for="(file, idx) in uploadedFiles" :key="idx" class="file-tag">
-                {{ file.name }}
-                <button @click="removeFile(idx)" class="remove-file">✖</button>
-              </span>
-            </div>
-          </div>
+      </el-col>
+      <el-col :span="4">
+        <div class="stat-item">
+          <div class="stat-title">已解除</div>
+          <div class="stat-value done" :class="stats.closed > 0 ? 'highlight' : ''">{{ stats.closed }}</div>
         </div>
-        <div class="form-actions">
-          <button @click="closeProcessModal" class="btn-outline">取消</button>
-          <button @click="submitProcess" class="btn-success">提交处理</button>
+      </el-col>
+      <el-col :span="4">
+        <div class="stat-item">
+          <div class="stat-title">今日新增</div>
+          <div class="stat-value today" :class="stats.today > 0 ? 'highlight' : ''">{{ stats.today }}</div>
         </div>
-      </div>
-    </div>
-
-    <!-- 查看详情模态框 -->
-    <div v-if="detailModalVisible" class="modal-overlay" @click.self="closeDetailModal">
-      <div class="modal-content">
-        <h3>📋 预警处理详情</h3>
-        <div class="detail-info">
-          <div class="info-row"><span class="label">设备名称:</span>{{ currentAlert?.deviceName }}</div>
-          <div class="info-row"><span class="label">预警时间:</span>{{ currentAlert?.time }}</div>
-          <div class="info-row"><span class="label">确认时间:</span>{{ currentAlert?.confirmTime || '-' }}</div>
-          <div class="info-row"><span class="label">处理人员:</span>{{ currentAlert?.handler || '-' }}</div>
-          <div class="info-row"><span class="label">处理结果:</span>{{ currentAlert?.processResult || '-' }}</div>
-          <div class="info-row" v-if="currentAlert?.attachments?.length">
-            <span class="label">附件:</span>
-            <div class="attachment-list">
-              <span v-for="(att, idx) in currentAlert.attachments" :key="idx" class="attachment">{{ att.name }}</span>
+      </el-col>
+    </el-row>
+
+    <!-- ========== 图表区域 ========== -->
+    <el-row :gutter="16" class="chart-row">
+      <el-col :span="12"><el-card><template #header><span>近 7 天趋势</span></template><div id="trendChart" style="height:240px"/></el-card></el-col>
+      <el-col :span="6"><el-card><template #header><span>级别分布</span></template><div id="levelChart" style="height:240px"/></el-card></el-col>
+      <el-col :span="6"><el-card><template #header><span>类型占比</span></template><div id="typeChart" style="height:240px"/></el-card></el-col>
+    </el-row>
+
+    <!-- ========= 查询表单 ========== -->
+    <el-card class="search-card">
+      <el-form :inline="true" :model="queryParams">
+        <el-form-item label="预警名称">
+          <el-input v-model="queryParams.warningName" placeholder="请输入预警名称" clearable style="width:200px"/>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select v-model="queryParams.status" placeholder="全部状态" clearable style="width:150px">
+            <el-option label="待确认" value="RELEASED"/>
+            <el-option label="待办" value="PENDING"/>
+            <el-option label="处理中" value="PROCESSING"/>
+            <el-option label="已处置" value="HANDLED"/>
+            <el-option label="已解除" value="CLOSED"/>
+            <el-option label="草稿" value="DRAFT"/>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="级别">
+          <el-select v-model="queryParams.warningLevel" placeholder="全部级别" clearable style="width:120px">
+            <el-option label="1 级 (蓝)" value="1"/>
+            <el-option label="2 级 (黄)" value="2"/>
+            <el-option label="3 级 (橙)" value="3"/>
+            <el-option label="4 级 (红)" value="4"/>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="类别">
+          <el-select v-model="queryParams.category" placeholder="全部类别" clearable style="width:120px">
+            <el-option label="普通预警" value="normal"/>
+            <el-option label="特殊预警" value="special"/>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="日期范围">
+          <el-date-picker v-model="queryParams.startDate" type="date" placeholder="开始日期" clearable value-format="YYYY-MM-DD" style="width:160px"/>
+          <span style="margin:0 8px;color:#909399;">至</span>
+          <el-date-picker v-model="queryParams.endDate" type="date" placeholder="结束日期" clearable value-format="YYYY-MM-DD" style="width:160px"/>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch"><el-icon><Search/></el-icon> 查询</el-button>
+          <el-button @click="handleReset"><el-icon><Refresh/></el-icon> 重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- ========== 预警列表 ========== -->
+    <el-card class="table-card">
+      <el-table :data="tableData" border stripe v-loading="loading" max-height="600">
+        <el-table-column prop="warningNo" label="预警编号" width="140" />
+        <el-table-column prop="warningName" label="预警名称" min-width="160" show-overflow-tooltip />
+        <el-table-column label="关联设备" width="160">
+          <template #default="{ row }">
+            <div v-if="row.deviceCode" class="device-cell">
+              <el-tag :type="row.warningLevel === '4' ? 'danger' : 'info'" size="small" effect="plain">{{ row.deviceCode }}</el-tag>
             </div>
-          </div>
+            <span v-else class="no-device">未关联</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="location" label="设备位置" min-width="140" show-overflow-tooltip />
+        <el-table-column label="类别" width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.category === 'normal' ? '' : 'warning'" size="small">
+              {{ row.category === 'normal' ? '普通预警' : '特殊预警' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="级别" width="100">
+          <template #default="{ row }">
+            <el-tag :type="getLevelType(row.warningLevel)" size="small">{{ getLevelText(row.warningLevel) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" width="100">
+          <template #default="{ row }">
+            <el-tag :type="getStatusType(row.status)" size="small">{{ getStatusText(row.status) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="publishTime" label="发布时间" width="170" sortable />
+        <el-table-column label="操作" width="180" fixed="right">
+          <template #default="{ row }">
+            <el-button v-if="isUnconfirmed(row.status)" type="primary" size="small" @click="openConfirm(row)">确认异常</el-button>
+            <el-button v-if="isUnconfirmed(row.status)" type="info" size="small" @click="confirmMisreport(row)">误报</el-button>
+            <el-button type="info" size="small" @click="openDetail(row.warningId)">详情</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,30]"
+                     @change="loadList" style="margin-top:16px;justify-content:flex-end" />
+    </el-card>
+
+    <!-- ========== 确认异常弹窗 ========== -->
+    <el-dialog v-model="confirmVisible" title="⚠️ 确认预警异常" width="600px" append-to-body destroy-on-close>
+      <el-descriptions :column="1" border>
+        <el-descriptions-item label="预警编号">{{ current.warningNo }}</el-descriptions-item>
+        <el-descriptions-item label="预警名称">{{ current.warningName }}</el-descriptions-item>
+        <el-descriptions-item label="预警内容">{{ current.warningContent || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="设备位置">{{ current.location || '-' }}</el-descriptions-item>
+      </el-descriptions>
+      <el-form label-width="100px" style="margin-top:16px">
+        <el-form-item label="关联设备">
+          <el-select v-model="deviceSelected" filterable remote clearable placeholder="请输入设备编码搜索" :remote-method="searchDevice" :loading="deviceLoading" style="width:100%">
+            <el-option v-for="dev in deviceOptions" :key="dev.equipmentId" :label="`${dev.equipmentName}(${dev.equipmentCode})`" :value="dev.equipmentId"/>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="备注信息">
+          <el-input v-model="confirmRemark" type="textarea" :rows="3" placeholder="填写异常情况描述(将写入工单描述)" maxlength="300" show-word-limit/>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="confirmVisible=false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitConfirm">确认提交</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- ========== 详情页抽屉 ========== -->
+    <el-drawer v-model="detailVisible" title="📋 预警处理详情" size="560px">
+      <template v-if="detail">
+        <el-descriptions :column="1" border size="small">
+          <el-descriptions-item label="预警编号">{{ detail.basicInfo.warning_no }}</el-descriptions-item>
+          <el-descriptions-item label="预警名称">{{ detail.basicInfo.warning_name }}</el-descriptions-item>
+          <el-descriptions-item label="预警类型">{{ detail.basicInfo.warning_type || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="预警级别">{{ getLevelText(detail.basicInfo.warning_level) }}</el-descriptions-item>
+          <el-descriptions-item label="发布人">{{ detail.basicInfo.publisher || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="发布时间">{{ detail.basicInfo.publish_time || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="状态">{{ getStatusText(detail.basicInfo.status) }}</el-descriptions-item>
+          <el-descriptions-item label="预警内容">{{ detail.basicInfo.warning_content }}</el-descriptions-item>
+        </el-descriptions>
+
+        <div class="detail-section-title">🔗 关联设备</div>
+        <el-descriptions :column="1" border size="small">
+          <el-descriptions-item label="设备编码">{{ detail.basicInfo.remark?.startsWith('AUTO|') ? detail.basicInfo.remark.split('|')[1] : '(手工录入,无设备关联)' }}</el-descriptions-item>
+          <el-descriptions-item label="设备位置">{{ detail.basicInfo.location || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="权属单位">{{ detail.basicInfo.ownership_unit || '-' }}</el-descriptions-item>
+        </el-descriptions>
+
+        <div class="detail-section-title">🕐 处置历史</div>
+        <el-timeline v-if="detail.disposalHistory?.length" style="padding:0 0 20px">
+          <el-timeline-item v-for="(item,idx) in detail.disposalHistory" :key="idx" :timestamp="item.create_time" :type="disposalTimelineType(item.disposal_type)">
+            <b>{{ disposalTypeText(item.disposal_type) }}</b>
+            <div>{{ item.disposal_content || '-' }}</div>
+            <div class="detail-sub">处置人:{{ item.disposal_user || '-' }}</div>
+          </el-timeline-item>
+        </el-timeline>
+        <div v-else class="detail-empty">暂无处置记录</div>
+
+        <div class="detail-section-title">📎 附件</div>
+        <el-upload v-if="false" action="#" list-type="text"/>
+        <div v-if="detail.attachmentList?.length" class="attachment-list">
+          <a v-for="(att,idx) in detail.attachmentList" :key="idx" class="attachment-link" :href="att.attachmentUrl" target="_blank">{{ att.attachmentName }}</a>
         </div>
-        <button @click="closeDetailModal" class="btn-primary" style="margin-top:16px;">关闭</button>
-      </div>
-    </div>
+        <div v-else class="detail-empty">暂无附件</div>
+
+        <div class="detail-section-title">📦 电子归档</div>
+        <el-descriptions v-if="detail.electronicArchive" :column="2" border size="small">
+          <el-descriptions-item label="归档编号">{{ detail.electronicArchive.archiveNo }}</el-descriptions-item>
+          <el-descriptions-item label="归档时间">{{ detail.electronicArchive.createTime }}</el-descriptions-item>
+        </el-descriptions>
+        <div v-else class="detail-empty">暂无归档(解除预警后自动生成)</div>
+      </template>
+    </el-drawer>
   </div>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
-
-// ==================== 预警数据模拟 ====================
-const alerts = ref([
-  { id: 1, deviceName: "压力传感器-PS001", location: "城东泵站", type: "normal", content: "设备离线,超过30分钟未回传数据", time: "2025-05-08 14:23:15", status: "unconfirmed", confirmTime: null, handler: null, processResult: null, attachments: [] },
-  { id: 2, deviceName: "流量监测仪-FL002", location: "城南枢纽", type: "normal", content: "设备在线异常,数据波动较大", time: "2025-05-08 13:45:22", status: "unconfirmed", confirmTime: null, handler: null, processResult: null, attachments: [] },
-  { id: 3, deviceName: "泄漏检测仪-LD005", location: "化工园区", type: "special", content: "可燃气浓度超阈值报警,当前值35%LEL", time: "2025-05-08 13:30:12", status: "processing", confirmTime: "2025-05-08 13:35:00", handler: "张工", processResult: null, attachments: [] },
-  { id: 4, deviceName: "温度传感器-TP003", location: "城西换热站", type: "normal", content: "设备离线", time: "2025-05-08 12:15:08", status: "confirmed", confirmTime: "2025-05-08 12:20:00", handler: null, processResult: null, attachments: [] },
-  { id: 5, deviceName: "水位监测仪-WL006", location: "东区排水口", type: "special", content: "水位超警戒线,当前水位3.2m", time: "2025-05-08 11:50:33", status: "resolved", confirmTime: "2025-05-08 11:55:00", handler: "李工", processResult: "已启动排水泵,水位降至正常", attachments: [{ name: "处理现场.jpg", url: "" }] },
-  { id: 6, deviceName: "压力传感器-PS008", location: "北环加压站", type: "normal", content: "设备离线(网络波动)", time: "2025-05-08 10:30:45", status: "misreport", confirmTime: "2025-05-08 10:35:00", handler: null, processResult: "网络恢复后设备正常,确认误报", attachments: [] },
-  { id: 7, deviceName: "燃气探测器-GD004", location: "城南调压站", type: "special", content: "燃气浓度异常波动", time: "2025-05-08 09:20:18", status: "confirmed", confirmTime: "2025-05-08 09:25:00", handler: null, processResult: null, attachments: [] }
-])
-
-// 地图点位数据(模拟)
-const mapPoints = ref([
-  { id: 1, name: "PS001", x: 150, y: 200, status: "offline" },
-  { id: 2, name: "FL002", x: 250, y: 150, status: "normal" },
-  { id: 3, name: "LD005", x: 350, y: 150, status: "special" },
-  { id: 4, name: "TP003", x: 450, y: 200, status: "normal" },
-  { id: 5, name: "WL006", x: 350, y: 300, status: "processing" },
-  { id: 6, name: "PS008", x: 250, y: 300, status: "normal" },
-  { id: 7, name: "GD004", x: 150, y: 300, status: "special" }
-])
-
-// 特殊预警地图标记
-const specialAlertsOnMap = computed(() => {
-  const specialAlerts = alerts.value.filter(a => a.type === 'special' && (a.status === 'unconfirmed' || a.status === 'confirmed' || a.status === 'processing'))
-  return specialAlerts.map((alert, idx) => {
-    const point = mapPoints.value.find(p => alert.deviceName.includes(p.name))
-    return { id: alert.id, x: point?.x || 200 + idx * 50, y: point?.y || 250, name: alert.deviceName }
-  })
-})
-
-// ==================== 查询参数 ====================
-const queryParams = ref({
-  startDate: '',
-  endDate: '',
-  status: '',
-  type: ''
-})
-
-const currentPage = ref(1)
-const pageSize = 8
-
-// 过滤后的预警列表
-const filteredAlerts = computed(() => {
-  let result = [...alerts.value]
-
-  if (queryParams.value.startDate) {
-    result = result.filter(a => a.time >= queryParams.value.startDate)
-  }
-  if (queryParams.value.endDate) {
-    result = result.filter(a => a.time <= queryParams.value.endDate + ' 23:59:59')
-  }
-  if (queryParams.value.status) {
-    result = result.filter(a => a.status === queryParams.value.status)
-  }
-  if (queryParams.value.type) {
-    result = result.filter(a => a.type === queryParams.value.type)
-  }
-
-  // 时间倒序
-  return result.sort((a, b) => new Date(b.time) - new Date(a.time))
-})
-
-// 分页数据
-const paginatedAlerts = computed(() => {
-  const start = (currentPage.value - 1) * pageSize
-  return filteredAlerts.value.slice(start, start + pageSize)
-})
-
-const totalPages = computed(() => Math.ceil(filteredAlerts.value.length / pageSize))
-
-// 统计数据
-const unconfirmedCount = computed(() => alerts.value.filter(a => a.status === 'unconfirmed').length)
-const processingCount = computed(() => alerts.value.filter(a => a.status === 'processing').length)
-const todayCount = computed(() => {
-  const today = new Date().toISOString().slice(0, 10)
-  return alerts.value.filter(a => a.time.startsWith(today)).length
-})
-
-// 周统计
-const weeklyStats = computed(() => {
-  const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
-  return days.map(() => Math.floor(Math.random() * 8) + 2)
-})
-const maxWeeklyCount = computed(() => Math.max(...weeklyStats.value, 10))
-
-// ==================== 辅助函数 ====================
-const getStatusText = (status) => {
-  const map = {
-    unconfirmed: '待确认',
-    confirmed: '已确认',
-    processing: '处理中',
-    resolved: '已清除',
-    misreport: '误报'
-  }
-  return map[status] || status
+import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
+import * as echarts from 'echarts'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Search, Refresh } from '@element-plus/icons-vue'
+import { getWarningDashboardStatisticsData, getWarningSearchData, getWarningFullDetailData, publishWarningData, resolveWarningData } from '@/api/pipeNetwork/earlyWarning'
+import { addManholeWorkOrder, getEquipmentFullPage } from '@/api/pipeNetwork/basic'
+
+// ========== 统计数据 ==========
+const stats = ref({ total:0, unconfirmed:0, processing:0, handled:0, closed:0, today:0 })
+
+// ========== 图表数据 ==========
+const chartData = ref({ trend:[], levels:[], types:[] })
+
+// ========== 查询参数 & 列表 ==========
+const queryParams = ref({ warningName:'', status:'', warningLevel:'', category:'', startDate:'', endDate:'' })
+const tableData = ref([])
+const loading = ref(false)
+const pageNum = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+
+async function loadStats() {
+  try {
+    const res = await getWarningDashboardStatisticsData()
+    if (!res.data) return
+    const b = res.data.basicStats
+    const t = res.data.trendStats || []
+    // releasedCount + pendingCount = 待确认
+    stats.value = {
+      total: Number(b.total || 0),
+      unconfirmed: Number(b.releasedCount || 0) + Number(b.pendingCount || 0),
+      processing: Number(b.processingCount || 0),
+      handled: Number(b.handledCount || 0),
+      closed: Number(b.closedCount || 0),
+      today: t.length ? Number(t[t.length-1].count || 0) : 0
+    }
+  } catch(e) { console.warn('加载统计数据失败', e) }
 }
 
-const getStatusClass = (status) => {
-  const map = {
-    unconfirmed: 'status-unconfirmed',
-    confirmed: 'status-confirmed',
-    processing: 'status-processing',
-    resolved: 'status-resolved',
-    misreport: 'status-misreport'
-  }
-  return map[status] || ''
+async function loadList() {
+  loading.value = true
+  try {
+    const params = { pageNum: pageNum.value, pageSize: pageSize.value }
+    if (queryParams.value.warningName) params.warningName = queryParams.value.warningName
+    if (queryParams.value.status) params.status = queryParams.value.status
+    if (queryParams.value.warningLevel) params.warningLevel = queryParams.value.warningLevel
+    if (queryParams.value.category === 'normal') params.publisher = '系统自动'
+    if (queryParams.value.category === 'special') params.excludePublisher = '系统自动'
+    if (queryParams.value.startDate) params.startTime = queryParams.value.startDate + ' 00:00:00'
+    if (queryParams.value.endDate) params.endTime = queryParams.value.endDate + ' 23:59:59'
+    
+    const res = await getWarningSearchData(params)
+    if (!res.data) return
+    tableData.value = (res.data.records || []).map(row => normalizeWarning(row))
+    total.value = Number(res.data.total || 0)
+  } catch(e) { ElMessage.error('加载预警列表失败'); console.error(e) }
+  finally { loading.value = false }
 }
 
-const getMapPointColor = (status) => {
-  const map = {
-    normal: '#27ae60',
-    offline: '#e74c3c',
-    special: '#e74c3c',
-    processing: '#f39c12'
-  }
-  return map[status] || '#95a5a6'
+// 图表数据填充(模拟)
+const fillChartData = () => {
+  if (chartData.value.trend?.length) return
+  const allStats = stats.value
+  const dates = [], counts = []
+  for(let i=6;i>=0;i--) { const d=new Date(); d.setDate(d.getDate()-i); dates.push((d.getMonth()+1)+'-'+d.getDate()) }
+  for(let i=6;i>=0;i--) { counts.push(Math.max(0, Math.floor(allStats.today * (0.5 + Math.random()*0.5)))) }
+  chartData.value.trend = dates.map((date,idx)=>({date,count:counts[idx]}))
+  chartData.value.levels = [1,2,3,4].map(lv=>({levelCode:String(lv), count: Math.max(1, Math.floor(Math.random()*allStats.total/5)+1)}))
+  chartData.value.types = [{typeCode:'drainage',typeName:'排水'}, {typeCode:'gas',typeName:'燃气管网'}, {typeCode:'fire',typeName:'消防水压'}]
+    .map(t=>({...t, count: Math.max(1, Math.floor(Math.random()*allStats.total/3)+2)}))
 }
 
-const showDeviceOnMap = (point) => {
-  const alert = alerts.value.find(a => a.deviceName.includes(point.name))
-  if (alert) {
-    alert(`设备: ${point.name}\n状态: ${alert.status}\n预警内容: ${alert.content}`)
-  }
-}
+// 图表渲染时的空数据状态
+const emptyChartData = { trend:[], levels:[], types:[] }
 
-// 查询相关
-const searchAlerts = () => {
-  currentPage.value = 1
-}
+// ========== 图表管理 ==========
+let trendChart, levelChart, typeChart
 
-const resetQuery = () => {
-  queryParams.value = { startDate: '', endDate: '', status: '', type: '' }
-  currentPage.value = 1
+const initCharts = () => {
+  // 如果数据为空,填充模拟数据
+  if (chartData.value.trend?.length === 0) fillChartData()
+  try { renderTrendChart() } catch(e){ console.warn('趋势图初始化失败:', e) }
+  try { renderLevelChart() } catch(e){ console.warn('级别图初始化失败:', e) }
+  try { renderTypeChart() } catch(e){ console.warn('类型图初始化失败:', e) }
 }
 
-// ==================== 预警确认 ====================
-const confirmModalVisible = ref(false)
-const currentAlert = ref(null)
-const confirmType = ref('device')
-const assignedPerson = ref('张工')
-
-const confirmAlert = (alert) => {
-  currentAlert.value = alert
-  confirmModalVisible.value = true
+// 下划线转驼峰 + 判断类别
+function normalizeWarning(row) {
+  const n = {}
+  Object.keys(row).forEach(key => {
+    const camel = key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
+    n[camel] = row[key]
+  })
+  n.category = n.publisher === '系统自动' ? 'normal' : 'special'
+  // 从 remark 字段提取设备编码 (AUTO|deviceCode|warningCode)
+  n.deviceCode = extractDeviceCodeStr(row)
+  return n
 }
 
-const markMisreport = (alert) => {
-  if (confirm('确认标记为误报吗?')) {
-    alert.status = 'misreport'
-    alert.confirmTime = new Date().toLocaleString()
-    alert.processResult = '网络/环境因素导致误报'
-    addSystemLog(alert, '误报确认')
+// 从预警行中提取设备编码
+function extractDeviceCodeStr(row) {
+  if (row.remark && typeof row.remark === 'string' && row.remark.startsWith('AUTO|')) {
+    const parts = row.remark.split('|')
+    if (parts.length >= 2) return parts[1]
   }
+  const m = (row.warning_content || row.warningContent || '').match(/设备\s*\[([^\]]+)\]/)
+  if (m) return m[1]
+  return ''
 }
 
-const closeConfirmModal = () => {
-  confirmModalVisible.value = false
-  currentAlert.value = null
+// ========== ECharts ==========
+const trendChartOpt = {
+  tooltip:{ trigger:'axis' },
+  legend:{ data:['新增预警'], top:0 },
+  grid:{ left:40,right:20,top:44,bottom:32 },
+  xAxis:{ type:'category', axisLabel:{ color:'#9098a6' }, axisLine:{ lineStyle:{ color:'#d0d5dd' } } },
+  yAxis:{ type:'value', name:'数量', axisLabel:{ color:'#9098a6' }, splitLine:{ lineStyle:{ color:'#f0f2f6',type:'dashed' } } },
+  series:[{ type:'line', smooth:true, data:[], lineStyle:{ color:'#e74c3c',width:2 }, itemStyle:{ color:'#e74c3c' } }]
 }
 
-const submitConfirm = () => {
-  if (currentAlert.value) {
-    if (confirmType.value === 'misreport') {
-      currentAlert.value.status = 'misreport'
-      currentAlert.value.processResult = '确认误报'
-    } else {
-      currentAlert.value.status = 'confirmed'
-      currentAlert.value.handler = assignedPerson.value
+const levelChartOpt = {
+  tooltip:{ trigger:'axis' },
+  grid:{ left:40,right:20,top:44,bottom:32 },
+  xAxis:{ type:'category', data: ['1 级','2 级','3 级','4 级'], axisLabel:{ color:'#9098a6' }, axisLine:{ lineStyle:{ color:'#d0d5dd' } } },
+  yAxis:{ type:'value', name:'数量', axisLabel:{ color:'#9098a6' }, splitLine:{ lineStyle:{ color:'#f0f2f6',type:'dashed' } } },
+  series:[{
+    type:'bar', barWidth:30,
+    data:[],
+    itemStyle:{
+      color:function(params){
+        const colors={1:'#e3f2fd',2:'#fff8e1',3:'#fff3e0',4:'#ffebee'};
+        return colors[params.dataIndex+1]||'#e3f2fd';
+      }
     }
-    currentAlert.value.confirmTime = new Date().toLocaleString()
-    addSystemLog(currentAlert.value, confirmType.value === 'device' ? '确认设备异常' : '误报确认')
-    closeConfirmModal()
-  }
+  }]
 }
 
-// ==================== 分配处理 ====================
-const assignHandler = (alert) => {
-  const handler = prompt('请指定处理人员(设备技术员)', '张工')
-  if (handler) {
-    alert.status = 'processing'
-    alert.handler = handler
-    addSystemLog(alert, `分配处理人员: ${handler}`)
-  }
+const typeChartOpt = {
+  tooltip:{ trigger:'item' },
+  legend:{ top:'10%', orient:'vertical', right:0 },
+  series:[{
+    type:'pie', radius:['40%','70%'], avoidLabelOverlap:false,
+    itemStyle:{ borderRadius:4, borderColor:'#fff', borderWidth:1 },
+    label:{ show:false },
+    data:[]
+  }]
 }
 
-// ==================== 预警处理 ====================
-const processModalVisible = ref(false)
-const processResult = ref('')
-const uploadedFiles = ref([])
+const resizeCharts = () => { trendChart?.resize(); levelChart?.resize(); typeChart?.resize() }
 
-const processAlert = (alert) => {
-  currentAlert.value = alert
-  processResult.value = alert.processResult || ''
-  uploadedFiles.value = alert.attachments || []
-  processModalVisible.value = true
+// ========== 图表渲染函数 ==========
+function renderTrendChart() {
+  const dom = document.getElementById('trendChart')
+  if (!dom || !trendChart) return
+  trendChart.dispose()
+  trendChart = echarts.init(dom)
+  const data = chartData.value.trend.length ? chartData.value.trend : emptyChartData.trend
+  trendChart.setOption({ ...trendChartOpt, xAxis:{ ...trendChartOpt.xAxis, data: data.map(i=>i.date) }, series:[{ ...trendChartOpt.series[0], data: data.map(i=>i.count || 0) }]
+  })
 }
 
-const handleFileUpload = (event) => {
-  const files = Array.from(event.target.files)
-  files.forEach(file => {
-    uploadedFiles.value.push({ name: file.name, size: file.size, url: URL.createObjectURL(file) })
+function renderLevelChart() {
+  const dom = document.getElementById('levelChart')
+  if (!dom || !levelChart) return
+  levelChart.dispose()
+  levelChart = echarts.init(dom)
+  const data = chartData.value.levels.length ? chartData.value.levels : emptyChartData.levels
+  levelChart.setOption({ ...levelChartOpt, series:[{ ...levelChartOpt.series[0], data: data.map(l=>l.count || 0) }]
   })
 }
 
-const removeFile = (idx) => {
-  uploadedFiles.value.splice(idx, 1)
+function renderTypeChart() {
+  const dom = document.getElementById('typeChart')
+  if (!dom || !typeChart) return
+  typeChart.dispose()
+  typeChart = echarts.init(dom)
+  const data = chartData.value.types.length ? chartData.value.types : emptyChartData.types
+  typeChart.setOption({ ...typeChartOpt, series:[{ ...typeChartOpt.series[0], data: data.map(t=>({ value:t.count || 0, name:t.typeName || t.typeCode })) }]
+  })
 }
 
-const closeProcessModal = () => {
-  processModalVisible.value = false
-  currentAlert.value = null
-  processResult.value = ''
-  uploadedFiles.value = []
+onMounted(async () => {
+  await loadStats()
+  await loadList()
+  await nextTick()
+  // 填充图表数据
+  const allStats = stats.value
+  const dates = [], counts = []
+  for(let i=6;i>=0;i--) { const d = new Date(); d.setDate(d.getDate()-i); dates.push((d.getMonth()+1)+'-'+d.getDate()) }
+  for(let i=6;i>=0;i--) { counts.push(Math.max(0, allStats.today - Math.floor(Math.random()*allStats.total*0.3))) }
+  chartData.value.trend = dates.map((date,idx)=>({date,count:counts[idx]}))
+  
+  // 级别图
+  chartData.value.levels = [1,2,3,4].map(lv=>({levelCode:String(lv), count: Math.floor(Math.random()*allStats.total/5)+1}))
+  // 类型图(无 API 时随机)
+  chartData.value.types = [{typeCode:'drainage',typeName:'排水'}, {typeCode:'gas',typeName:'燃气管网'}, {typeCode:'fire',typeName:'消防水压'}]
+    .map(t=>({...t, count: Math.floor(Math.random()*allStats.total/3)+2}))
+
+  initCharts()
+  window.addEventListener('resize', resizeCharts)
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', resizeCharts)
+  trendChart?.dispose()
+  levelChart?.dispose()
+  typeChart?.dispose()
+})
+
+// ========== 辅助函数 ==========
+const isUnconfirmed = (status) => ['RELEASED','PENDING'].includes(status)
+const getStatusText = (s) => ({ DRAFT:'草稿', PENDING:'待办', PROCESSING:'处理中', RELEASED:'待确认', HANDLED:'已处置', CLOSED:'已解除' }[s]||s)
+const getStatusType = (s) => ({ DRAFT:'info', PENDING:'danger', PROCESSING:'warning', RELEASED:'warning', HANDLED:'primary', CLOSED:'success' }[s]||'')
+const getLevelText = (lv) => ({ '1':'1 级 (蓝)','2':'2 级 (黄)','3':'3 级 (橙)','4':'4 级 (红)' }[lv]||'-' )
+const getLevelType = (lv) => ({ '1':'','2':'warning','3':'warning','4':'danger' }[lv]||'')
+const disposalTypeText = (t) => ({ RELEASE:'发布预警', UPGRADE:'升级预警', RESOLVE:'解除预警', RETURN:'退回重办', SUPERVISION:'督办', INSTRUCTION:'批示' }[t]||t)
+const disposalTimelineType = (t) => ({ RESOLVE:'success', UPGRADE:'warning', RETURN:'danger' }[t]||'primary')
+
+// ========== 操作处理 ==========
+function handleSearch() { pageNum.value=1; loadList() }
+function handleReset() { queryParams.value={warningName:'',status:'',warningLevel:'',category:'',startDate:'',endDate:''}; pageNum.value=1; loadList() }
+
+// ========== 确认异常 ==========
+const confirmVisible = ref(false)
+const current = ref({})
+const deviceSelected = ref(null)
+const deviceOptions = ref([])
+const deviceLoading = ref(false)
+const confirmingRemark = ref('')
+const submitting = ref(false)
+
+function openConfirm(row) {
+  current.value = row
+  deviceSelected.value = null
+  deviceOptions.value = []
+  confirmingRemark.value = ''
+  extractDeviceCode(row)
+  confirmVisible.value = true
 }
 
-const submitProcess = () => {
-  if (currentAlert.value) {
-    currentAlert.value.status = 'resolved'
-    currentAlert.value.processResult = processResult.value
-    currentAlert.value.attachments = [...uploadedFiles.value]
-    addSystemLog(currentAlert.value, '处理完成,预警已清除')
-    closeProcessModal()
+function extractDeviceCode(row) {
+  // remark 格式:"AUTO|equipmentCode|warningCode"
+  if (row.remark && row.remark.startsWith('AUTO|')) {
+    const parts = row.remark.split('|')
+    if (parts.length >= 2) searchDevice(parts[1])
+  } else {
+    const m = (row.warningContent || '').match(/设备 \[([^\]]+)\]/)
+    if (m) searchDevice(m[1])
   }
 }
 
-// ==================== 查看详情 ====================
-const detailModalVisible = ref(false)
+async function searchDevice(query) {
+  if (!query) return
+  deviceLoading.value = true
+  try {
+    const res = await getEquipmentFullPage(1, 20, { equipmentCode: query })
+    if (res.data) deviceOptions.value = (res.data.rows || []) || []
+  } catch(e) { console.warn('设备搜索失败', e) }
+  finally { deviceLoading.value = false }
+}
 
-const viewDetail = (alert) => {
-  currentAlert.value = alert
-  detailModalVisible.value = true
+async function submitConfirm() {
+  if (!deviceSelected.value) { ElMessage.warning('请选择关联设备'); return }
+  if (!confirmingRemark.value.trim()) { ElMessage.warning('请填写异常描述'); return }
+  
+  submitting.value = true
+  try {
+    await publishWarningData(current.value.warningId)
+    const orderDesc = `预警「${current.value.warningName}」异常确认;${confirmingRemark.value}`
+    const res = await addManholeWorkOrder({ alarmId: current.value.warningId, deviceId: deviceSelected.value, orderType: 1, orderLevel: getWorkOrderLevel(current.value.warningLevel), orderDesc: orderDesc.slice(0,500) })
+    ElMessage.success(`✅ 预警已确认 | 工单已生成 (WO-...) | 请前往工单系统继续处理`)
+    confirmVisible.value = false
+    loadList()
+  } catch(e) { ElMessage.error('确认失败:'+(e.msg||e.message||'重试')); console.error(e) }
+  finally { submitting.value=false }
 }
 
-const closeDetailModal = () => {
-  detailModalVisible.value = false
-  currentAlert.value = null
+function getWorkOrderLevel(warningLevel) { const lv = Number(warningLevel); return lv>=3?1:(lv===2?2:3) }
+
+// ========== 误报处理 ==========
+function confirmMisreport(row) {
+  ElMessageBox.prompt('请输入误报原因', '误报确认', {
+    inputPlaceholder: '例如:传感器误判、测试数据等',
+    inputValue: '',
+    inputPattern: /.*/,
+    message: '误报后将直接解除该预警',
+    confirmButtonText:'确认误报',
+    cancelButtonText:'取消'
+  }).then(async ({value}) => {
+    await resolveWarningData({ warningId: row.warningId, disposalContent: '误报:'+value })
+    ElMessage.success('已标记为误报并解除')
+    loadList()
+  }).catch(()=>{})
 }
 
-// 模拟系统日志
-const addSystemLog = (alert, action) => {
-  console.log(`[日志] ${new Date().toLocaleString()} - ${alert.deviceName}: ${action}`)
-  // 实际可扩展为独立日志组件
+// ========== 详情抽屉 ==========
+const detailVisible = ref(false)
+const detail = ref(null)
+
+async function openDetail(warningId) {
+  detailVisible.value = true
+  detail.value = null
+  try {
+    const res = await getWarningFullDetailData(warningId)
+    if (res.data) detail.value = res.data
+  } catch(e) { ElMessage.error('详情加载中'); console.error(e) }
 }
 
-// 地图点击处理
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', resizeCharts)
+  trendChart?.dispose()
+  levelChart?.dispose()
+  typeChart?.dispose()
+})
+
+onMounted(async () => {
+  await nextTick()
+  await Promise.all([loadStats(), loadList()])
+  await fillChartData()
+  initCharts()
+  window.addEventListener('resize', resizeCharts)
+})
 </script>
 
 <style scoped>
-* { margin: 0; padding: 0; box-sizing: border-box; }
-
-.prototype-container {
-  max-width: 1600px;
-  margin: 0 auto;
-  background: #f0f4f8;
-  border-radius: 28px;
-  box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.15);
-  overflow: hidden;
-  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
+.warning-container { padding: 20px; min-height: calc(100vh - 84px); }
+.stat-row { margin-bottom: 16px; }
+.stat-item {
+  background: #fff; border-radius: 12px; padding: 12px 10px; text-align: center;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.06); border: 1px solid #eef2f6;
+  transition: transform 0.2s, box-shadow 0.2s;
 }
-
-/* 头部 */
-.header {
-  background: #0a2b3c;
-  padding: 16px 28px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 16px;
-  border-bottom: 3px solid #e74c3c;
+.stat-item:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.1); }
+.stat-title { font-size: 13px; color: #7a8a9a; margin-bottom: 4px; }
+.stat-value { font-size: 28px; font-weight: 700; color: #3a4a5a; transition: all 0.3s; }
+.stat-value.primary { color: #409eff; }
+.stat-value.warning { color: #e6a23c; }
+.stat-value.info { color: #909399; }
+.stat-value.success { color: #67c23a; }
+.stat-value.done { color: #27ae60; }
+.stat-value.today { color: #9b59b6; }
+.stat-value.highlight { animation: pulse 1.5s ease-in-out; }
+@keyframes pulse { 0%{transform:scale(1)}50%{transform:scale(1.12)}100%{transform:scale(1)} }
+
+.chart-row { margin-bottom: 16px; }
+.chart-card { min-height: 300px; }
+.chart-empty {
+  display: flex; align-items: center; justify-content: center;
+  height: 240px; color: #c0c4cc; font-size: 14px;
+  flex-direction: column; gap: 8px;
 }
-
-.logo h2 { color: white; font-weight: 600; font-size: 1.3rem; }
-.logo p { color: #bdd8e8; font-size: 0.7rem; }
-
-.header-stats { display: flex; gap: 16px; }
-.stat-card { background: rgba(255,255,255,0.15); padding: 6px 16px; border-radius: 40px; text-align: center; color: white; }
-.stat-card.warning { background: #e74c3c; }
-.stat-card.success { background: #27ae60; }
-.stat-value { font-weight: 700; font-size: 1.2rem; margin-right: 6px; }
-.stat-label { font-size: 0.7rem; opacity: 0.9; }
-
-/* 布局 */
-.main-layout { display: flex; flex-wrap: wrap; }
-.left-panel { width: 420px; background: #f9fbfd; padding: 20px; }
-.right-panel { flex: 1; background: #ffffff; padding: 20px; min-width: 500px; }
-
-/* 卡片 */
-.card { background: white; border-radius: 20px; border: 1px solid #e6edf3; margin-bottom: 20px; overflow: hidden; }
-.card-header { background: #f9fafc; padding: 14px 18px; border-bottom: 1px solid #eef2f6; font-weight: 700; font-size: 0.85rem; color: #1d5a78; display: flex; justify-content: space-between; align-items: center; }
-.card-body { padding: 16px; }
-
-/* 物联感知地图 */
-.iot-map { background: #1a3a4a; border-radius: 16px; padding: 12px; }
-.map-container { position: relative; }
-.map-svg { width: 100%; height: 280px; background: linear-gradient(135deg, #1a3a4a 0%, #0e2a32 100%); border-radius: 12px; cursor: pointer; }
-.map-legend { display: flex; gap: 16px; margin-top: 12px; padding: 8px 12px; background: rgba(0,0,0,0.6); border-radius: 20px; color: white; font-size: 0.65rem; }
-.legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; }
-.legend-dot.normal { background: #27ae60; }
-.legend-dot.offline { background: #e74c3c; }
-.legend-dot.special { background: #e74c3c; box-shadow: 0 0 8px #e74c3c; }
-.legend-dot.processing { background: #f39c12; }
-.pulse-ring { animation: pulse-ring 1.5s infinite; }
-
-/* 趋势图 */
-.trend-chart { display: flex; justify-content: space-around; align-items: flex-end; height: 100px; gap: 8px; }
-.trend-bar { display: flex; flex-direction: column; align-items: center; flex: 1; }
-.bar { background: #3498db; width: 100%; border-radius: 4px 4px 0 0; transition: height 0.3s; }
-.bar-label { font-size: 0.6rem; margin-top: 4px; color: #6f8ea8; }
-
-/* 筛选栏 */
-.filter-group { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
-.filter-input, .filter-select { padding: 6px 10px; border-radius: 20px; border: 1px solid #cbdde6; font-size: 0.7rem; }
-.filter-sep { font-size: 0.7rem; color: #6f8ea8; }
-.btn-primary, .btn-outline, .btn-sm { padding: 5px 12px; border-radius: 30px; font-size: 0.7rem; cursor: pointer; }
-.btn-primary { background: #3498db; border: none; color: white; }
-.btn-outline { background: white; border: 1px solid #3498db; color: #3498db; }
-.btn-confirm { background: #27ae60; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-.btn-misreport { background: #95a5a6; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-.btn-assign { background: #f39c12; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-.btn-process { background: #3498db; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-.btn-view { background: #1abc9c; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-.btn-success { background: #27ae60; color: white; border: none; padding: 8px 16px; border-radius: 30px; cursor: pointer; }
-
-/* 表格 */
-.alert-table { overflow-x: auto; }
-.data-table { width: 100%; border-collapse: collapse; font-size: 0.7rem; }
-.data-table th { background: #f8fafc; padding: 10px 8px; text-align: left; font-weight: 600; color: #2c5a72; border-bottom: 1px solid #e2edf2; }
-.data-table td { padding: 10px 8px; border-bottom: 1px solid #f0f4f7; vertical-align: middle; }
-.time-cell { white-space: nowrap; font-family: monospace; }
-.content-cell { max-width: 200px; overflow: hidden; text-overflow: ellipsis; }
-.action-cell { white-space: nowrap; display: flex; gap: 6px; flex-wrap: wrap; }
-.empty-row { text-align: center; padding: 30px; color: #8aaec2; }
-.list-count { font-size: 0.65rem; font-weight: normal; color: #6f8ea8; }
-
-/* 状态标签 */
-.alert-type { padding: 2px 8px; border-radius: 20px; font-size: 0.65rem; }
-.type-normal { background: #e8f4fd; color: #3498db; }
-.type-special { background: #fef3c7; color: #f39c12; }
-.status-badge { padding: 2px 8px; border-radius: 20px; font-size: 0.65rem; }
-.status-unconfirmed { background: #fef3c7; color: #f39c12; }
-.status-confirmed { background: #e3f2fd; color: #3498db; }
-.status-processing { background: #fce4ec; color: #e74c3c; }
-.status-resolved { background: #d5f5e3; color: #27ae60; }
-.status-misreport { background: #f5f5f5; color: #95a5a6; }
-.completed-badge { color: #27ae60; font-size: 0.65rem; }
-
-/* 分页 */
-.pagination { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
-.page-btn { background: white; border: 1px solid #cbdde6; padding: 4px 10px; border-radius: 20px; font-size: 0.7rem; cursor: pointer; }
-.page-btn.active { background: #3498db; color: white; border-color: #3498db; }
-
-/* 模态框 */
-.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
-.modal-content { background: white; border-radius: 24px; width: 450px; max-width: 90%; padding: 24px; }
-.form-group { margin-bottom: 16px; }
-.form-label { font-size: 0.7rem; font-weight: 600; margin-bottom: 6px; display: block; color: #2c5a72; }
-.form-select, .form-textarea { width: 100%; padding: 8px 12px; border-radius: 12px; border: 1px solid #cbdde6; font-size: 0.75rem; }
-.form-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px; }
-.confirm-info, .process-info, .detail-info { background: #f8fafc; border-radius: 16px; padding: 12px; margin-bottom: 16px; }
-.info-row { font-size: 0.75rem; margin-bottom: 8px; }
-.info-row .label { font-weight: 600; width: 90px; display: inline-block; color: #2c5a72; }
-
-/* 上传区域 */
-.upload-area { border: 1px dashed #cbdde6; border-radius: 12px; padding: 12px; }
-.upload-btn { background: #3498db; color: white; padding: 6px 14px; border-radius: 30px; font-size: 0.7rem; cursor: pointer; display: inline-block; }
-.file-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
-.file-tag { background: #eef2fa; padding: 4px 8px; border-radius: 20px; font-size: 0.65rem; display: inline-flex; align-items: center; gap: 6px; }
-.remove-file { background: none; border: none; color: #e74c3c; cursor: pointer; }
-
-.live-badge { font-size: 0.65rem; background: #e74c3c; color: white; padding: 2px 10px; border-radius: 20px; animation: blink 1s infinite; }
-
-@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } }
-@keyframes pulse-ring { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
-</style>
+.chart-empty::before { content:'\1F4CA'; font-size: 32px; opacity: 0.5; }
+
+.search-card { margin-bottom: 16px; }
+.table-card { min-height: 600px; }
+.device-cell { display:flex; align-items:center; gap:4px; }
+.no-device { color:#c0c4cc; font-size:12px; }
+
+.detail-section-title { font-weight:600;font-size:14px;color:#2c3e50;margin:16px 0 10px;padding-bottom:8px;border-bottom:1px solid #eef2f6 }
+.detail-sub { font-size:12px;color:#9098a6;margin-top:4px }
+.detail-empty { font-size:13px;color:#9098a6;padding:12px 0 }
+.attachment-list { display:flex;flex-direction:column;gap:6px }
+.attachment-link { font-size:13px;color:#3498db;text-decoration:none }
+.attachment-link:hover { text-decoration:underline }
+
+@media (max-width: 1300px) {
+  .stat-row { display:flex; gap:10px; flex-wrap:wrap; }
+  .stat-row > * { flex-shrink:0; width: calc(33.33% - 14px) !important; }
+}
+</style>

+ 790 - 687
src/views/subSystem/pipeNetwork/pworkOrder/Pdayinspect.vue

@@ -1,768 +1,871 @@
-<!--日常巡检-->
+<!--日常巡检工单(orderType=2,参考排水/燃气工单:搜索卡 + 左列表 + 右详情 tabs + 按钮操作 + 部门树派单)-->
+<!--数据源:/api/work-order/*(orderType=2 过滤;equipmentType 为空 = 所有设备)-->
 <template>
-  <div class="prototype-container">
-    <!-- 头部 -->
-    <div class="header">
-      <div class="logo">
-        <h2>📋 日常巡检管理系统</h2>
-        <p>巡检任务管理 | 问题跟踪 | 闭环处理 | 多维度查询</p>
+  <div class="order-container">
+    <!-- 顶部搜索区(参考排水/燃气工单) -->
+    <el-card class="search-card" shadow="never">
+      <div class="search-form">
+        <el-form :model="queryParams" ref="queryFormRef" :inline>
+          <el-form-item label="优先级">
+            <el-select v-model="queryParams.orderLevel" placeholder="全部优先级" clearable style="width: 120px">
+              <el-option v-for="(v, k) in ORDER_LEVEL_MAP" :key="k" :label="v.text" :value="Number(k)"/>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="状态">
+            <el-select v-model="queryParams.orderStatus" placeholder="全部状态" clearable style="width: 120px">
+              <el-option v-for="(v, k) in ORDER_STATUS_MAP" :key="k" :label="v.text" :value="Number(k)"/>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="关键词">
+            <el-input v-model="queryParams.keyword" placeholder="巡检内容/工单编号" clearable style="width: 200px" @keyup.enter="handleSearch"/>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="handleSearch">查询</el-button>
+            <el-button @click="handleReset">重置</el-button>
+            <el-button type="primary" plain @click="openAddDialog">➕ 新增巡检</el-button>
+          </el-form-item>
+        </el-form>
       </div>
-      <div class="header-stats">
-        <div class="stat-card pending">
-          <span class="stat-value">{{ pendingCount }}</span>
-          <span class="stat-label">待处理</span>
-        </div>
-        <div class="stat-card processing">
-          <span class="stat-value">{{ processingCount }}</span>
-          <span class="stat-label">处理中</span>
-        </div>
-        <div class="stat-card completed">
-          <span class="stat-value">{{ completedCount }}</span>
-          <span class="stat-label">已完成</span>
-        </div>
-      </div>
-    </div>
-
-    <div class="main-layout">
-      <!-- 左侧:新增巡检表单 -->
-      <div class="left-panel">
-        <div class="card">
-          <div class="card-header">
-            ➕ 新增日常巡检
-            <span class="card-tip">请填写巡检信息</span>
-          </div>
-          <div class="card-body">
-            <form @submit.prevent="addInspection">
-              <div class="form-group">
-                <label class="form-label">巡检内容 *</label>
-                <textarea v-model="newInspection.content" class="form-textarea" rows="2" placeholder="请输入巡检内容,如:泵房设备检查、仪表读数记录、管道巡查等..."></textarea>
-              </div>
-              <div class="form-row">
-                <div class="form-group half">
-                  <label class="form-label">巡检时间 *</label>
-                  <input type="datetime-local" v-model="newInspection.inspectTime" class="form-input" required>
-                </div>
-                <div class="form-group half">
-                  <label class="form-label">巡检人员</label>
-                  <input v-model="newInspection.inspector" class="form-input" placeholder="巡检人姓名">
-                </div>
-              </div>
-              <div class="form-group">
-                <label class="form-label">巡检问题</label>
-                <textarea v-model="newInspection.issue" class="form-textarea" rows="2" placeholder="如发现问题请详细描述,无问题可不填..."></textarea>
-              </div>
-              <div class="form-group">
-                <label class="form-label">处理过程</label>
-                <textarea v-model="newInspection.process" class="form-textarea" rows="2" placeholder="问题处理过程描述..."></textarea>
-              </div>
-              <div class="form-row">
-                <div class="form-group half">
-                  <label class="form-label">是否处理完成</label>
-                  <select v-model="newInspection.isCompleted" class="form-select">
-                    <option :value="false">未完成</option>
-                    <option :value="true">已完成</option>
-                  </select>
+    </el-card>
+
+    <!-- 主容器:左列表 + 右详情(参考排水/燃气工单) -->
+    <el-row :gutter="16" class="main-row">
+      <!-- 左侧:巡检工单列表 -->
+      <el-col :span="8" class="col-fill">
+        <el-card class="box-card" shadow="always">
+          <template #header>
+            <div class="card-header">
+              <span>巡检工单列表</span>
+            </div>
+          </template>
+          <el-scrollbar class="list-scroll">
+            <div class="order-list">
+              <div
+                v-for="order in tableData"
+                :key="order.orderId"
+                class="order-item"
+                :class="{ 'active': selectedOrderId === order.orderId }"
+                @click="handleSelectOrder(order)"
+              >
+                <div class="order-header">
+                  <span class="order-no">{{ order.orderNo }}</span>
+                  <el-tag :type="orderStatusType(order.orderStatus)" size="small" effect="light" class="status-badge">{{ orderStatusText(order.orderStatus) }}</el-tag>
                 </div>
-                <div class="form-group half">
-                  <label class="form-label">完成时间</label>
-                  <input type="datetime-local" v-model="newInspection.completeTime" class="form-input" :disabled="!newInspection.isCompleted">
+                <div class="order-body">
+                  <div class="order-tags">
+                    <el-tag :type="orderLevelType(order.orderLevel)" size="small" effect="plain">{{ orderLevelText(order.orderLevel) }}</el-tag>
+                    <el-tag v-if="order.hazardNo" size="small" effect="plain">隐患 {{ order.hazardNo }}</el-tag>
+                  </div>
+                  <div class="order-desc" v-if="order.orderDesc">{{ order.orderDesc }}</div>
+                  <div class="device-code">巡检人员:{{ userNames(order.receiveUser) || '-' }}</div>
+                  <div class="create-time">计划巡检:{{ timeText(order.planFinishTime) }}</div>
                 </div>
               </div>
-              <div class="form-actions">
-                <button type="submit" class="btn-primary">✅ 提交巡检记录</button>
-                <button type="button" @click="resetForm" class="btn-outline">重置</button>
-              </div>
-            </form>
-          </div>
-        </div>
-
-        <!-- 快速统计 -->
-        <div class="card">
-          <div class="card-header">📊 巡检统计概览</div>
-          <div class="card-body">
-            <div class="stats-grid">
-              <div class="stat-circle">
-                <div class="circle-num">{{ completionRate }}%</div>
-                <div class="circle-label">完成率</div>
-              </div>
-              <div class="stat-list">
-                <div class="stat-row"><span>本月巡检</span><span>{{ monthlyCount }}</span></div>
-                <div class="stat-row"><span>本周巡检</span><span>{{ weeklyCount }}</span></div>
-                <div class="stat-row"><span>问题发现率</span><span>{{ issueRate }}%</span></div>
-              </div>
+              <el-empty v-if="tableData.length === 0" description="暂无数据" :image-size="100"/>
             </div>
+          </el-scrollbar>
+          <div class="pagination-wrapper">
+            <el-pagination
+              v-model:current-page="pageNum"
+              v-model:page-size="pageSize"
+              :total="total"
+              layout="total, prev, pager, next"
+              :page-sizes="[10, 20, 50, 100]"
+              @size-change="handleSizeChange"
+              @current-change="handlePageChange"
+              class="pagination"
+            />
           </div>
-        </div>
-      </div>
-
-      <!-- 右侧:巡检列表 -->
-      <div class="right-panel">
-        <div class="card">
-          <div class="card-header">
-            📋 日常巡检记录列表
-            <span class="list-count">共 {{ filteredInspections.length }} 条记录</span>
-          </div>
-          <div class="card-body">
-            <!-- 查询筛选栏 -->
-            <div class="filter-bar">
-              <div class="filter-group">
-                <label class="filter-label">巡检时间范围</label>
-                <div class="date-range">
-                  <input type="date" v-model="queryParams.startDate" class="filter-input" placeholder="开始日期">
-                  <span class="filter-sep">至</span>
-                  <input type="date" v-model="queryParams.endDate" class="filter-input" placeholder="结束日期">
-                </div>
-              </div>
-              <div class="filter-group">
-                <label class="filter-label">完成状态</label>
-                <select v-model="queryParams.status" class="filter-select">
-                  <option value="">全部</option>
-                  <option value="pending">待处理</option>
-                  <option value="processing">处理中</option>
-                  <option value="completed">已完成</option>
-                </select>
-              </div>
-              <div class="filter-group">
-                <label class="filter-label">关键词</label>
-                <input type="text" v-model="queryParams.keyword" class="filter-input" placeholder="巡检内容/问题">
-              </div>
-              <div class="filter-actions">
-                <button @click="searchInspections" class="btn-primary btn-sm">🔍 查询</button>
-                <button @click="resetQuery" class="btn-outline btn-sm">重置</button>
-              </div>
-            </div>
-
-            <!-- 巡检列表表格 -->
-            <div class="inspection-table-wrapper">
-              <table class="data-table">
-                <thead>
-                <tr>
-                  <th>巡检时间</th>
-                  <th>巡检内容</th>
-                  <th>巡检人员</th>
-                  <th>巡检问题</th>
-                  <th>处理过程</th>
-                  <th>完成状态</th>
-                  <th>完成时间</th>
-                  <th>操作</th>
-                </tr>
-                </thead>
-                <tbody>
-                <tr v-for="item in paginatedInspections" :key="item.id">
-                  <td class="time-cell">{{ formatDateTime(item.inspectTime) }}</td>
-                  <td class="content-cell">{{ truncateText(item.content, 40) }}</td>
-                  <td>{{ item.inspector || '-' }}</td>
-                  <td class="issue-cell">
-                    <span v-if="item.issue" class="issue-badge">有</span>
-                    <span v-else class="no-issue">无</span>
-                    <span class="issue-text">{{ truncateText(item.issue, 30) }}</span>
-                  </td>
-                  <td class="process-cell">{{ truncateText(item.process, 30) || '-' }}</td>
-                  <td>
-                      <span class="status-badge" :class="getStatusClass(item)">
-                        {{ getStatusText(item) }}
-                      </span>
-                  </td>
-                  <td class="time-cell">{{ formatDateTime(item.completeTime) || '-' }}</td>
-                  <td class="action-cell">
-                    <button @click="viewDetail(item)" class="btn-view">查看</button>
-                    <button v-if="!item.isCompleted" @click="openProcessModal(item)" class="btn-process">处理</button>
-                    <button @click="editInspection(item)" class="btn-edit">编辑</button>
-                    <button @click="deleteInspection(item)" class="btn-delete">删除</button>
-                  </td>
-                </tr>
-                <tr v-if="filteredInspections.length === 0">
-                  <td colspan="8" class="empty-row">暂无巡检记录,请点击新增进行添加</td>
-                </tr>
-                </tbody>
-              </table>
+        </el-card>
+      </el-col>
+
+      <!-- 右侧:巡检工单详情 -->
+      <el-col :span="16" class="col-fill">
+        <el-card v-loading="detailLoading" class="box-card" shadow="always">
+          <template #header>
+            <div class="card-header">
+              <span v-if="selectedOrder && selectedOrder.orderId">巡检详情 - {{ selectedOrder.orderNo }}</span>
+              <span v-else>请选择巡检工单查看详情</span>
             </div>
+          </template>
+
+          <div v-if="selectedOrder && selectedOrder.orderId" class="detail-body">
+            <el-tabs v-model="activeTab" class="detail-tabs">
+              <!-- 工单信息 -->
+              <el-tab-pane label="工单信息" name="info">
+                <el-descriptions :column="2" border size="small">
+                  <el-descriptions-item label="工单编号">{{ selectedOrder.orderNo }}</el-descriptions-item>
+                  <el-descriptions-item label="工单状态">
+                    <el-tag :type="orderStatusType(selectedOrder.orderStatus)" size="small">{{ orderStatusText(selectedOrder.orderStatus) }}</el-tag>
+                  </el-descriptions-item>
+                  <el-descriptions-item label="关联隐患编号">{{ selectedOrder.hazardNo || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="设备编号">{{ selectedOrder.deviceCode || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="优先级">
+                    <el-tag :type="orderLevelType(selectedOrder.orderLevel)" size="small">{{ orderLevelText(selectedOrder.orderLevel) }}</el-tag>
+                  </el-descriptions-item>
+                  <el-descriptions-item label="巡检人员">{{ userNames(selectedOrder.receiveUser) || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="巡检内容" :span="2">{{ selectedOrder.orderDesc || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="计划巡检时间">{{ timeText(selectedOrder.planFinishTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="派单时间">{{ timeText(selectedOrder.dispatchTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="实际完成时间">{{ timeText(selectedOrder.realFinishTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="验收时间">{{ timeText(selectedOrder.verifyTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="创建时间">{{ timeText(selectedOrder.createTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="更新时间">{{ timeText(selectedOrder.updateTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="处置结果" :span="2">{{ selectedOrder.repairResult || '-' }}</el-descriptions-item>
+                </el-descriptions>
+
+                <!-- 关联设备信息 -->
+                <div v-if="detailDevice" class="device-section">
+                  <h4 class="section-title">🔗 关联设备</h4>
+                  <el-descriptions :column="2" border size="small">
+                    <el-descriptions-item label="设备类型">{{ detailDevice.equipmentTypeName || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="设备编码">{{ detailDevice.equipmentCode || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="安装位置">{{ detailDevice.equipmentLocation || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="管点地址">{{ detailDevice.pointAddress || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="设备厂家">{{ detailDevice.manufacturer || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="所属管线">{{ detailDevice.networkName || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="维护人员">{{ detailDevice.maintainer || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="联系方式">{{ detailDevice.maintainerPhone || '-' }}</el-descriptions-item>
+                  </el-descriptions>
+                </div>
 
-            <!-- 分页 -->
-            <div class="pagination" v-if="totalPages > 1">
-              <button v-for="page in totalPages" :key="page" class="page-btn" :class="{ active: currentPage === page }"
-                      @click="currentPage = page">{{ page }}</button>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- 查看详情模态框 -->
-    <div v-if="detailModalVisible" class="modal-overlay" @click.self="closeDetailModal">
-      <div class="modal-content">
-        <h3>📋 巡检详情</h3>
-        <div class="detail-info">
-          <div class="info-row"><span class="label">巡检时间:</span>{{ formatDateTime(currentItem?.inspectTime) }}</div>
-          <div class="info-row"><span class="label">巡检人员:</span>{{ currentItem?.inspector || '-' }}</div>
-          <div class="info-row"><span class="label">巡检内容:</span>{{ currentItem?.content }}</div>
-          <div class="info-row"><span class="label">巡检问题:</span>{{ currentItem?.issue || '无' }}</div>
-          <div class="info-row"><span class="label">处理过程:</span>{{ currentItem?.process || '-' }}</div>
-          <div class="info-row"><span class="label">完成状态:</span>
-            <span class="status-badge" :class="getStatusClass(currentItem)">{{ getStatusText(currentItem) }}</span>
+                <RepairImagePanel :before-images="selectedOrder.maintenanceBeforeImg" :after-images="selectedOrder.maintenanceAfterImg" />
+
+                <!-- 工单操作(参考排水/燃气工单按钮组) -->
+                <div class="action-section">
+                  <h4 class="section-title">工单操作</h4>
+                  <div class="action-buttons">
+                    <el-button v-if="canOperate.dispatch(selectedOrder)" type="primary" @click="handleDispatch">下派任务</el-button>
+                    <el-button v-if="canOperate.verify(selectedOrder)" type="warning" @click="handleVerify">核查</el-button>
+                    <el-button v-if="canOperate.process(selectedOrder)" type="success" plain @click="handleProcess">
+                      📷 处理(可传处理前照片)
+                    </el-button>
+                    <el-button v-if="canOperate.complete(selectedOrder)" type="danger" plain @click="handleComplete">
+                      ✅ 提交验收(可传维修后照片)
+                    </el-button>
+                    <el-button v-if="canOperate.close(selectedOrder)" type="success" @click="handleClose">验收通过</el-button>
+                    <el-button v-if="canOperate.cancel(selectedOrder)" type="info" plain @click="handleCancel">取消工单</el-button>
+                  </div>
+                </div>
+              </el-tab-pane>
+
+              <!-- 处理历史 -->
+              <el-tab-pane label="处理历史" name="history">
+                <div class="timeline-scroll">
+                  <el-timeline v-if="currentOrderLogs && currentOrderLogs.length > 0">
+                    <el-timeline-item
+                      v-for="log in currentOrderLogs"
+                      :key="log.logId || log.id"
+                      :timestamp="timeText(log.operTime)"
+                      placement="top"
+                      :type="orderLogType(log.operType)"
+                      size="large"
+                    >
+                      <el-card shadow="hover">
+                        <div class="timeline-content">
+                          <div class="timestamp">{{ timeText(log.operTime) }}</div>
+                          <div class="operator-info">
+                            <span class="operator-name">{{ log.operUserName || '-' }}</span>
+                            <span class="oper-type">· {{ orderLogText(log.operType) }}</span>
+                          </div>
+                          <div class="oper-desc">{{ log.operDesc || '-' }}</div>
+                        </div>
+                      </el-card>
+                    </el-timeline-item>
+                  </el-timeline>
+                  <el-empty v-else description="暂无处理记录" :image-size="100"/>
+                </div>
+              </el-tab-pane>
+            </el-tabs>
           </div>
-          <div class="info-row"><span class="label">完成时间:</span>{{ formatDateTime(currentItem?.completeTime) || '-' }}</div>
-          <div class="info-row" v-if="currentItem?.remark"><span class="label">备注:</span>{{ currentItem?.remark }}</div>
-        </div>
-        <div class="form-actions">
-          <button @click="closeDetailModal" class="btn-primary">关闭</button>
-        </div>
-      </div>
-    </div>
-
-    <!-- 处理模态框 -->
-    <div v-if="processModalVisible" class="modal-overlay" @click.self="closeProcessModal">
-      <div class="modal-content" style="width:500px;">
-        <h3>🔧 处理巡检问题</h3>
-        <div class="process-info-summary">
-          <div><span class="label">巡检时间:</span>{{ formatDateTime(currentItem?.inspectTime) }}</div>
-          <div><span class="label">巡检内容:</span>{{ currentItem?.content }}</div>
-          <div><span class="label">巡检问题:</span>{{ currentItem?.issue }}</div>
-        </div>
-        <div class="form-group">
-          <label class="form-label">处理过程 *</label>
-          <textarea v-model="processData.process" class="form-textarea" rows="3" placeholder="请详细描述处理过程..."></textarea>
-        </div>
-        <div class="form-group">
-          <label class="form-label">是否完成</label>
-          <select v-model="processData.isCompleted" class="form-select">
-            <option :value="true">已完成</option>
-            <option :value="false">未完成(继续跟进)</option>
-          </select>
-        </div>
-        <div class="form-group" v-if="processData.isCompleted">
-          <label class="form-label">完成时间</label>
-          <input type="datetime-local" v-model="processData.completeTime" class="form-input">
+
+          <el-empty v-else description="请从左侧选择巡检工单" :image-size="150"/>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 新增巡检弹窗(提交后自动派单) -->
+    <el-dialog v-model="addVisible" title="➕ 新增日常巡检" width="560px" append-to-body destroy-on-close>
+      <el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="110px">
+        <el-form-item label="巡检内容" prop="orderDesc">
+          <el-input v-model="addForm.orderDesc" type="textarea" :rows="3" placeholder="如:泵房设备检查、仪表读数记录、管道巡查" maxlength="300" show-word-limit/>
+        </el-form-item>
+        <el-form-item label="计划巡检时间" prop="planFinishTime">
+          <el-date-picker v-model="addForm.planFinishTime" type="datetime" placeholder="计划巡检时间" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/>
+        </el-form-item>
+        <el-form-item label="巡检人员" prop="receiveUserId">
+          <el-select v-model="addForm.receiveUserId" filterable placeholder="选择巡检人员(提交后自动派单)" style="width:100%">
+            <el-option v-for="u in userOptions" :key="u.userId" :label="u.nickName + '(' + u.userName + ')'" :value="u.userId"/>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="addVisible=false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitAdd">提交并派单</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 派单弹窗(部门树 + 用户多选,参考排水/燃气工单) -->
+    <el-dialog v-model="dispatchDialogVisible" title="下派任务 - 选择接单人" width="750px" append-to-body>
+      <div style="display: flex; gap: 16px; min-height: 350px;">
+        <div style="width: 280px; border-right: 1px solid #e4e7ed; padding-right: 12px;">
+          <el-input v-model="deptFilterText" placeholder="筛选部门" clearable size="small" style="margin-bottom: 8px"/>
+          <el-tree
+            ref="deptTreeRef"
+            :data="deptTreeData"
+            :props="{ label: 'deptName', children: 'children' }"
+            :filter-node-method="filterDeptNode"
+            node-key="deptId"
+            highlight-current
+            @node-click="handleDeptClick"
+            style="font-size: 14px"
+          />
         </div>
-        <div class="form-actions">
-          <button @click="closeProcessModal" class="btn-outline">取消</button>
-          <button @click="submitProcess" class="btn-primary">提交处理</button>
+        <div style="flex: 1; min-width: 0;">
+          <el-table
+            ref="userTableRef"
+            :data="userList"
+            border size="small"
+            max-height="300"
+            row-key="userId"
+            @selection-change="handleSelectionChange"
+            @row-click="handleRowClick"
+          >
+            <el-table-column type="selection" width="40" align="center" :reserve-selection="true"/>
+            <el-table-column prop="userName" label="用户名" width="100"/>
+            <el-table-column prop="nickName" label="昵称" width="100"/>
+            <el-table-column prop="dept.deptName" label="部门" show-overflow-tooltip/>
+          </el-table>
+          <div v-if="userList.length === 0" style="text-align: center; color: #909399; padding: 40px 0;">请先在左侧选择部门</div>
         </div>
       </div>
-    </div>
-
-    <!-- 编辑模态框 -->
-    <div v-if="editModalVisible" class="modal-overlay" @click.self="closeEditModal">
-      <div class="modal-content" style="width:550px;">
-        <h3>✏️ 编辑巡检记录</h3>
-        <form @submit.prevent="saveEdit">
-          <div class="form-group">
-            <label class="form-label">巡检内容 *</label>
-            <textarea v-model="editForm.content" class="form-textarea" rows="2" required></textarea>
-          </div>
-          <div class="form-row">
-            <div class="form-group half">
-              <label class="form-label">巡检时间 *</label>
-              <input type="datetime-local" v-model="editForm.inspectTime" class="form-input" required>
-            </div>
-            <div class="form-group half">
-              <label class="form-label">巡检人员</label>
-              <input v-model="editForm.inspector" class="form-input">
-            </div>
-          </div>
-          <div class="form-group">
-            <label class="form-label">巡检问题</label>
-            <textarea v-model="editForm.issue" class="form-textarea" rows="2"></textarea>
-          </div>
-          <div class="form-group">
-            <label class="form-label">处理过程</label>
-            <textarea v-model="editForm.process" class="form-textarea" rows="2"></textarea>
-          </div>
-          <div class="form-row">
-            <div class="form-group half">
-              <label class="form-label">是否处理完成</label>
-              <select v-model="editForm.isCompleted" class="form-select">
-                <option :value="false">未完成</option>
-                <option :value="true">已完成</option>
-              </select>
-            </div>
-            <div class="form-group half">
-              <label class="form-label">完成时间</label>
-              <input type="datetime-local" v-model="editForm.completeTime" class="form-input" :disabled="!editForm.isCompleted">
-            </div>
-          </div>
-          <div class="form-actions">
-            <button type="button" @click="closeEditModal" class="btn-outline">取消</button>
-            <button type="submit" class="btn-primary">保存修改</button>
-          </div>
-        </form>
-      </div>
-    </div>
+      <el-form label-width="80px" style="margin-top: 16px">
+        <el-form-item label="备注"><el-input v-model="dispatchForm.remarks" placeholder="请输入派单备注"/></el-form-item>
+      </el-form>
+      <template #footer>
+        <span style="float: left; line-height: 32px; color: #606266;">
+          已选人员:
+          <span v-if="selectedUserNames.length > 0" style="color: #409eff; font-weight: bold;">{{ selectedUserNames.join('、') }}</span>
+          <span v-else style="color: #909399">未选择</span>
+          ({{ dispatchForm.handlerIds.length }})
+        </span>
+        <el-button @click="dispatchDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitDispatch">确认</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 核查/处理弹窗(处理过程写入 solution,后端存入 repairResult 展示) -->
+    <el-dialog v-model="actionDialogVisible" :title="actionTitle" width="600px" append-to-body>
+      <el-form label-width="90px">
+        <el-form-item label="结果说明" required>
+          <el-input v-model="actionFormData.solution" type="textarea" :rows="5" :placeholder="actionTitle === '核查工单' ? '请输入核查结论(属实/不属实、现场情况)' : '请输入巡检问题及处理过程、处理结果'" maxlength="500" show-word-limit/>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="actionDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitAction">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 提交验收弹窗 -->
+    <el-dialog v-model="completeDialogVisible" title="提交验收" width="500px" append-to-body>
+      <el-form label-width="80px">
+        <el-form-item label="验收意见">
+          <el-input v-model="completeForm.result" type="textarea" :rows="4" placeholder="请填写巡检结果确认说明" maxlength="500" show-word-limit/>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="completeDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitComplete">提交</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 验收通过/结案弹窗 -->
+    <el-dialog v-model="closeDialogVisible" title="验收通过" width="500px" append-to-body>
+      <el-form label-width="80px">
+        <el-form-item label="验收结果">
+          <el-input v-model="closeForm.result" type="textarea" :rows="4" placeholder="请填写巡检验收结果说明" maxlength="500" show-word-limit/>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="closeDialogVisible = false">取消</el-button>
+        <el-button type="success" :loading="submitting" @click="submitClose">确认验收</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
-
-// ==================== 巡检数据 ====================
-const inspections = ref([
-  {
-    id: 1,
-    content: "城东泵房日常巡检:检查设备运行状态、仪表读数",
-    inspectTime: "2025-05-08T09:00:00",
-    inspector: "张建国",
-    issue: "2号水泵有异响,需进一步检查",
-    process: "已联系维修人员,待拆解检查",
-    isCompleted: false,
-    completeTime: null,
-    remark: ""
-  },
-  {
-    id: 2,
-    content: "化工园区燃气管道巡查",
-    inspectTime: "2025-05-08T14:30:00",
-    inspector: "李卫东",
-    issue: "",
-    process: "",
-    isCompleted: true,
-    completeTime: "2025-05-08T16:30:00",
-    remark: "管道运行正常"
-  },
-  {
-    id: 3,
-    content: "城南枢纽流量计校准检查",
-    inspectTime: "2025-05-07T10:15:00",
-    inspector: "王敏",
-    issue: "流量计显示偏差较大",
-    process: "已重新校准,数据恢复正常",
-    isCompleted: true,
-    completeTime: "2025-05-07T11:45:00",
-    remark: ""
-  },
-  {
-    id: 4,
-    content: "北环加压站设备巡检",
-    inspectTime: "2025-05-07T15:20:00",
-    inspector: "陈方",
-    issue: "压力表读数异常",
-    process: "正在排查原因",
-    isCompleted: false,
-    completeTime: null,
-    remark: ""
-  },
-  {
-    id: 5,
-    content: "城西换热站温度传感器检查",
-    inspectTime: "2025-05-06T11:00:00",
-    inspector: "赵伟",
-    issue: "温度传感器数据滞后",
-    process: "已更换传感器,恢复正常",
-    isCompleted: true,
-    completeTime: "2025-05-06T15:30:00",
-    remark: ""
-  }
-])
-
-// 新增表单数据
-const newInspection = ref({
-  content: '',
-  inspectTime: new Date().toISOString().slice(0, 16),
-  inspector: '',
-  issue: '',
-  process: '',
-  isCompleted: false,
-  completeTime: ''
-})
+import { ref, reactive, computed, watch, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import RepairImagePanel from '@/components/RepairImagePanel'
+import { listWorkOrder, getWorkOrderDetail, addWorkOrder, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder, cancelWorkOrder } from '@/api/work/order'
+import { getEquipmentFullPage } from '@/api/pipeNetwork/basic'
+import { listDept } from '@/api/system/dept'
+import { listUser } from '@/api/system/user'
+import { handleTree } from '@/utils/ruoyi'
+import { ORDER_STATUS_MAP, ORDER_LEVEL_MAP, orderStatusText, orderStatusType, orderLevelText, orderLevelType, orderLogText, orderLogType, canOperate, timeText } from './workOrderDict'
+
+// ========== 查询 & 列表(orderType=2 日常巡检) ==========
+const listLoading = ref(false)
+const tableData = ref([])
+const pageNum = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const queryFormRef = ref(null)
+const queryParams = reactive({ orderLevel: null, orderStatus: null, keyword: '' })
+
+async function handleSearch() {
+  listLoading.value = true
+  try {
+    const params = { orderType: 2 }
+    if (queryParams.orderLevel) params.orderLevel = queryParams.orderLevel
+    if (queryParams.orderStatus) params.orderStatus = queryParams.orderStatus
+    if (queryParams.keyword) params.keyword = queryParams.keyword
+    const res = await listWorkOrder(params, pageNum.value, pageSize.value)
+    if (res.data) {
+      tableData.value = res.data.records || []
+      total.value = Number(res.data.total || 0)
+    }
+  } catch(e) { ElMessage.error('查询失败'); console.error(e) }
+  finally { listLoading.value = false }
+}
 
-// 查询参数
-const queryParams = ref({
-  startDate: '',
-  endDate: '',
-  status: '',
-  keyword: ''
-})
+function handleReset() {
+  queryFormRef.value && queryFormRef.value.resetFields()
+  queryParams.orderLevel = null
+  queryParams.orderStatus = null
+  queryParams.keyword = ''
+  pageNum.value = 1
+  selectedOrderId.value = null
+  selectedOrder.value = {}
+  handleSearch()
+}
 
-// 分页
-const currentPage = ref(1)
-const pageSize = 6
-
-// 模态框状态
-const detailModalVisible = ref(false)
-const processModalVisible = ref(false)
-const editModalVisible = ref(false)
-const currentItem = ref(null)
-
-// 处理表单数据
-const processData = ref({
-  process: '',
-  isCompleted: true,
-  completeTime: new Date().toISOString().slice(0, 16)
-})
+// ========== 选中工单 & 详情(合并 res.data.order + logs) ==========
+const selectedOrderId = ref(null)
+const selectedOrder = ref({})
+const activeTab = ref('info')
+const currentOrderLogs = ref([])
+const detailLoading = ref(false)
+const detailDevice = ref(null)
+
+async function handleSelectOrder(order) {
+  selectedOrderId.value = order.orderId
+  selectedOrder.value = { ...order }
+  activeTab.value = 'info'
+  currentOrderLogs.value = []
+  detailDevice.value = null
+  await loadOrderDetail(order.orderId)
+}
 
-// 编辑表单数据
-const editForm = ref({
-  id: null,
-  content: '',
-  inspectTime: '',
-  inspector: '',
-  issue: '',
-  process: '',
-  isCompleted: false,
-  completeTime: ''
-})
+async function loadOrderDetail(orderId) {
+  detailLoading.value = true
+  try {
+    const res = await getWorkOrderDetail(orderId)
+    if (res.code === 200 && res.data) {
+      // 后端返回 { order, logs } 两层结构,必须合并 order 层级
+      selectedOrder.value = { ...selectedOrder.value, ...res.data.order }
+      currentOrderLogs.value = res.data.logs || []
+      if (res.data.order?.deviceCode) {
+        loadDeviceInfo(res.data.order.deviceCode)
+      }
+    }
+  } catch(e) { ElMessage.error('加载巡检详情失败'); console.error(e) }
+  finally { detailLoading.value = false }
+}
 
-// ==================== 辅助函数 ====================
-const formatDateTime = (dateTimeStr) => {
-  if (!dateTimeStr) return ''
-  return dateTimeStr.replace('T', ' ')
+async function loadDeviceInfo(codeOrId) {
+  try {
+    const res = await getEquipmentFullPage(1, 1, { equipmentCode: codeOrId })
+    const rows = res.data?.records || res.data?.rows || []
+    if (rows.length) { detailDevice.value = rows[0]; return }
+  } catch(e) { console.warn('加载设备信息失败', e) }
 }
 
-const truncateText = (text, maxLen) => {
-  if (!text) return ''
-  return text.length > maxLen ? text.substring(0, maxLen) + '...' : text
+function handleSizeChange() {
+  pageNum.value = 1
+  selectedOrderId.value = null
+  selectedOrder.value = {}
+  handleSearch()
 }
 
-// 计算统计
-const pendingCount = computed(() => inspections.value.filter(i => !i.isCompleted && i.issue).length)
-const processingCount = computed(() => inspections.value.filter(i => !i.isCompleted && i.process).length)
-const completedCount = computed(() => inspections.value.filter(i => i.isCompleted).length)
-
-const monthlyCount = computed(() => {
-  const now = new Date()
-  const month = now.getMonth()
-  const year = now.getFullYear()
-  return inspections.value.filter(i => {
-    const d = new Date(i.inspectTime)
-    return d.getMonth() === month && d.getFullYear() === year
-  }).length
-})
+function handlePageChange() {
+  selectedOrderId.value = null
+  selectedOrder.value = {}
+  handleSearch()
+}
 
-const weeklyCount = computed(() => {
-  const now = new Date()
-  const weekStart = new Date(now)
-  weekStart.setDate(now.getDate() - now.getDay())
-  return inspections.value.filter(i => new Date(i.inspectTime) >= weekStart).length
-})
+// ========== 用户映射(巡检人员 ID → 姓名) ==========
+const userOptions = ref([])
+const userMap = ref({})
+function userNames(ids) {
+  if (!ids) return ''
+  return String(ids).split(',').map(id => userMap.value[id] || id).join('、')
+}
+async function loadUsers() {
+  try {
+    const res = await listUser({ pageNum: 1, pageSize: 200 })
+    if (res.rows) {
+      userOptions.value = res.rows
+      userMap.value = Object.fromEntries(res.rows.map(u => [u.userId, u.nickName || u.userName]))
+    }
+  } catch(e) { console.warn('加载用户列表失败', e) }
+}
 
-const issueRate = computed(() => {
-  const withIssue = inspections.value.filter(i => i.issue).length
-  return inspections.value.length === 0 ? 0 : Math.round((withIssue / inspections.value.length) * 100)
-})
+// ========== 新增巡检(提交后自动派单) ==========
+const addVisible = ref(false)
+const addFormRef = ref(null)
+const addForm = reactive({ orderDesc: '', planFinishTime: '', receiveUserId: null })
+const addRules = {
+  orderDesc: [{ required: true, message: '请填写巡检内容', trigger: 'blur' }],
+  planFinishTime: [{ required: true, message: '请选择巡检时间', trigger: 'change' }],
+  receiveUserId: [{ required: true, message: '请选择巡检人员', trigger: 'change' }]
+}
+const submitting = ref(false)
 
-const completionRate = computed(() => {
-  return inspections.value.length === 0 ? 0 : Math.round((completedCount.value / inspections.value.length) * 100)
-})
+function openAddDialog() {
+  addForm.orderDesc = ''
+  addForm.planFinishTime = ''
+  addForm.receiveUserId = null
+  addVisible.value = true
+}
+
+async function submitAdd() {
+  await addFormRef.value.validate().catch(() => { throw new Error('校验失败') })
+  submitting.value = true
+  try {
+    const res = await addWorkOrder({ orderType: 2, orderDesc: addForm.orderDesc, planFinishTime: addForm.planFinishTime })
+    const orderId = res.data
+    if (!orderId) throw new Error('未返回工单ID')
+    await dispatchWorkOrder({ orderId: Number(orderId), handlerIds: [addForm.receiveUserId], remarks: '日常巡检自动派单' })
+    ElMessage.success('巡检工单已创建并派单')
+    addVisible.value = false
+    handleSearch()
+  } catch(e) { ElMessage.error('创建失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+  finally { submitting.value = false }
+}
+
+// ========== 派单(部门树 + 用户多选,参考排水/燃气工单) ==========
+const dispatchDialogVisible = ref(false)
+const dispatchForm = reactive({ handlerIds: [], remarks: '' })
+const deptTreeRef = ref(null)
+const userTableRef = ref(null)
+const deptTreeData = ref([])
+const deptFilterText = ref('')
+const userList = ref([])
+const selectedUserList = ref([])
+
+watch(deptFilterText, (val) => { deptTreeRef.value && deptTreeRef.value.filter(val) })
+function filterDeptNode(value, data) {
+  if (!value) return true
+  return data.deptName.includes(value)
+}
 
-// 获取状态文本
-const getStatusText = (item) => {
-  if (item.isCompleted) return '已完成'
-  if (item.process) return '处理中'
-  if (item.issue) return '待处理'
-  return '正常'
+async function loadDeptTree() {
+  if (deptTreeData.value.length > 0) return
+  try {
+    const res = await listDept()
+    deptTreeData.value = handleTree(res.data || [], 'deptId')
+  } catch(e) { console.warn('加载部门失败', e) }
 }
 
-const getStatusClass = (item) => {
-  if (item.isCompleted) return 'status-completed'
-  if (item.process) return 'status-processing'
-  if (item.issue) return 'status-pending'
-  return 'status-normal'
+async function handleDeptClick(node) {
+  try {
+    userTableRef.value && userTableRef.value.clearSelection()
+    selectedUserList.value = []
+    dispatchForm.handlerIds = []
+    const res = await listUser({ deptId: node.deptId, pageNum: 1, pageSize: 100 })
+    userList.value = res.rows || []
+  } catch(e) { console.warn('加载部门用户失败', e) }
 }
 
-// 过滤后的巡检列表
-const filteredInspections = computed(() => {
-  let result = [...inspections.value]
+function handleSelectionChange(selection) {
+  selectedUserList.value = selection
+  dispatchForm.handlerIds = selection.map(item => item.userId)
+}
+
+function handleRowClick(row) {
+  userTableRef.value && userTableRef.value.toggleRowSelection(row)
+}
 
-  if (queryParams.value.startDate) {
-    result = result.filter(i => i.inspectTime >= queryParams.value.startDate)
+const selectedUserNames = computed(() => selectedUserList.value.map(item => item.nickName || item.userName))
+
+function handleDispatch() {
+  if (!canOperate.dispatch(selectedOrder.value)) {
+    ElMessage.warning('仅可对待派单工单进行派单操作')
+    return
   }
-  if (queryParams.value.endDate) {
-    result = result.filter(i => i.inspectTime <= queryParams.value.endDate + 'T23:59:59')
+  Object.assign(dispatchForm, { handlerIds: [], remarks: '' })
+  selectedUserList.value = []
+  userList.value = []
+  loadDeptTree()
+  dispatchDialogVisible.value = true
+}
+
+async function submitDispatch() {
+  if (!dispatchForm.handlerIds || dispatchForm.handlerIds.length === 0) {
+    ElMessage.warning('请选择至少一个接单人')
+    return
   }
-  if (queryParams.value.status) {
-    result = result.filter(i => {
-      if (queryParams.value.status === 'pending') return !i.isCompleted && i.issue && !i.process
-      if (queryParams.value.status === 'processing') return !i.isCompleted && i.process
-      if (queryParams.value.status === 'completed') return i.isCompleted
-      return true
+  submitting.value = true
+  try {
+    await dispatchWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      handlerIds: dispatchForm.handlerIds,
+      remarks: dispatchForm.remarks
     })
-  }
-  if (queryParams.value.keyword) {
-    const kw = queryParams.value.keyword.toLowerCase()
-    result = result.filter(i =>
-        i.content.toLowerCase().includes(kw) ||
-        (i.issue && i.issue.toLowerCase().includes(kw))
-    )
-  }
-  // 按巡检时间倒序
-  return result.sort((a, b) => new Date(b.inspectTime) - new Date(a.inspectTime))
-})
-
-const paginatedInspections = computed(() => {
-  const start = (currentPage.value - 1) * pageSize
-  return filteredInspections.value.slice(start, start + pageSize)
-})
+    ElMessage.success('派单成功!已向 ' + dispatchForm.handlerIds.length + ' 人下发任务')
+    dispatchDialogVisible.value = false
+    refreshAfterAction()
+  } catch(e) { ElMessage.error('派单失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+  finally { submitting.value = false }
+}
 
-const totalPages = computed(() => Math.ceil(filteredInspections.value.length / pageSize))
+// ========== 核查/处理(处理过程写入 solution,后端存入 repairResult) ==========
+const actionDialogVisible = ref(false)
+const actionTitle = ref('')
+const actionFormData = reactive({ solution: '' })
 
-// ==================== 新增巡检 ====================
-const addInspection = () => {
-  if (!newInspection.value.content) {
-    alert('请填写巡检内容')
+function handleVerify() {
+  if (!canOperate.verify(selectedOrder.value)) {
+    ElMessage.warning('当前工单不允许核查')
     return
   }
-  if (!newInspection.value.inspectTime) {
-    alert('请选择巡检时间')
+  actionTitle.value = '核查工单'
+  actionFormData.solution = ''
+  actionDialogVisible.value = true
+}
+
+function handleProcess() {
+  if (!canOperate.process(selectedOrder.value)) {
+    ElMessage.warning('当前工单不允许处理')
     return
   }
+  actionTitle.value = '处理工单'
+  actionFormData.solution = ''
+  actionDialogVisible.value = true
+}
 
-  const newId = Math.max(...inspections.value.map(i => i.id), 0) + 1
-  const newRecord = {
-    id: newId,
-    content: newInspection.value.content,
-    inspectTime: newInspection.value.inspectTime,
-    inspector: newInspection.value.inspector || '',
-    issue: newInspection.value.issue || '',
-    process: newInspection.value.process || '',
-    isCompleted: newInspection.value.isCompleted,
-    completeTime: newInspection.value.isCompleted ? newInspection.value.completeTime : null,
-    remark: ''
+async function submitAction() {
+  if (!actionFormData.solution.trim()) {
+    ElMessage.warning('请填写说明')
+    return
   }
-
-  inspections.value.unshift(newRecord)
-  alert('巡检记录已添加')
-  resetForm()
-  currentPage.value = 1
+  submitting.value = true
+  try {
+    const data = {
+      orderId: selectedOrder.value.orderId,
+      handleResult: 'solved',
+      solution: actionFormData.solution
+    }
+    if (actionTitle.value === '核查工单') {
+      await verifyWorkOrder(data)
+    } else {
+      await processWorkOrder(data)
+    }
+    ElMessage.success(actionTitle.value + '成功')
+    actionDialogVisible.value = false
+    refreshAfterAction()
+  } catch(e) { ElMessage.error('操作失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+  finally { submitting.value = false }
 }
 
-const resetForm = () => {
-  newInspection.value = {
-    content: '',
-    inspectTime: new Date().toISOString().slice(0, 16),
-    inspector: '',
-    issue: '',
-    process: '',
-    isCompleted: false,
-    completeTime: ''
+// ========== 提交验收(后端 completeOrder 要求状态 4) ==========
+const completeDialogVisible = ref(false)
+const completeForm = reactive({ result: '' })
+
+function handleComplete() {
+  if (!canOperate.complete(selectedOrder.value)) {
+    ElMessage.warning('当前工单不允许提交验收')
+    return
   }
+  completeForm.result = ''
+  completeDialogVisible.value = true
 }
 
-// ==================== 查询 ====================
-const searchInspections = () => {
-  currentPage.value = 1
+async function submitComplete() {
+  if (!completeForm.result.trim()) {
+    ElMessage.warning('请填写验收意见')
+    return
+  }
+  submitting.value = true
+  try {
+    await completeWorkOrder({ orderId: selectedOrder.value.orderId, result: completeForm.result })
+    ElMessage.success('提交验收成功')
+    completeDialogVisible.value = false
+    refreshAfterAction()
+  } catch(e) { ElMessage.error('提交失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+  finally { submitting.value = false }
 }
 
-const resetQuery = () => {
-  queryParams.value = {
-    startDate: '',
-    endDate: '',
-    status: '',
-    keyword: ''
+// ========== 验收通过/结案 ==========
+const closeDialogVisible = ref(false)
+const closeForm = reactive({ result: '' })
+
+function handleClose() {
+  if (!canOperate.close(selectedOrder.value)) {
+    ElMessage.warning('当前工单不允许验收通过')
+    return
   }
-  currentPage.value = 1
+  closeForm.result = ''
+  closeDialogVisible.value = true
 }
 
-// ==================== 查看详情 ====================
-const viewDetail = (item) => {
-  currentItem.value = item
-  detailModalVisible.value = true
+async function submitClose() {
+  if (!closeForm.result.trim()) {
+    ElMessage.warning('请填写验收结果')
+    return
+  }
+  submitting.value = true
+  try {
+    await closeWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      result: closeForm.result
+    })
+    ElMessage.success('验收通过,巡检工单已结案')
+    closeDialogVisible.value = false
+    refreshAfterAction()
+  } catch(e) { ElMessage.error('验收失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+  finally { submitting.value = false }
 }
 
-const closeDetailModal = () => {
-  detailModalVisible.value = false
-  currentItem.value = null
+// ========== 取消工单 ==========
+function handleCancel() {
+  ElMessageBox.confirm(`确定取消巡检工单「${selectedOrder.value.orderNo}」吗?`, '取消工单', { type: 'warning' })
+    .then(async () => {
+      try {
+        await cancelWorkOrder(selectedOrder.value.orderId)
+        ElMessage.success('工单已取消')
+        selectedOrderId.value = null
+        selectedOrder.value = {}
+        handleSearch()
+      } catch(e) { ElMessage.error('取消失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+    }).catch(() => {})
 }
 
-// ==================== 处理巡检 ====================
-const openProcessModal = (item) => {
-  currentItem.value = item
-  processData.value = {
-    process: item.process || '',
-    isCompleted: true,
-    completeTime: new Date().toISOString().slice(0, 16)
+/** 操作完成后刷新列表并重新加载当前选中工单 */
+async function refreshAfterAction() {
+  await handleSearch()
+  const fresh = tableData.value.find(o => o.orderId === selectedOrderId.value)
+  if (fresh) {
+    await handleSelectOrder(fresh)
+  } else {
+    selectedOrderId.value = null
+    selectedOrder.value = {}
   }
-  processModalVisible.value = true
 }
 
-const closeProcessModal = () => {
-  processModalVisible.value = false
-  currentItem.value = null
-}
+onMounted(() => {
+  loadUsers()
+  handleSearch()
+})
+</script>
 
-const submitProcess = () => {
-  if (!processData.value.process && processData.value.isCompleted) {
-    alert('请填写处理过程')
-    return
+<style scoped lang="scss">
+.order-container {
+  padding: 0 16px 16px;
+
+  .search-card {
+    margin-bottom: 16px;
+
+    :deep(.el-card__body) {
+      padding: 16px;
+    }
   }
 
-  if (currentItem.value) {
-    currentItem.value.process = processData.value.process
-    currentItem.value.isCompleted = processData.value.isCompleted
-    if (processData.value.isCompleted) {
-      currentItem.value.completeTime = processData.value.completeTime || new Date().toISOString()
+  .search-form {
+    .el-form {
+      display: flex;
+      flex-wrap: wrap;
+      align-items: flex-start;
+      gap: 12px;
+
+      .el-form-item {
+        margin-bottom: 0;
+        min-width: 150px;
+
+        &:last-child {
+          margin-left: auto;
+        }
+      }
     }
-    alert('处理记录已更新')
-    closeProcessModal()
   }
-}
 
-// ==================== 编辑巡检 ====================
-const editInspection = (item) => {
-  editForm.value = {
-    id: item.id,
-    content: item.content,
-    inspectTime: item.inspectTime,
-    inspector: item.inspector || '',
-    issue: item.issue || '',
-    process: item.process || '',
-    isCompleted: item.isCompleted,
-    completeTime: item.completeTime || ''
+  .main-row {
+    height: calc(100vh - 200px);
+
+    .col-fill {
+      height: 100%;
+    }
   }
-  editModalVisible.value = true
-}
 
-const closeEditModal = () => {
-  editModalVisible.value = false
-}
+  .box-card {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
 
-const saveEdit = () => {
-  if (!editForm.value.content || !editForm.value.inspectTime) {
-    alert('请填写巡检内容和巡检时间')
-    return
+    :deep(.el-card__body) {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      overflow: hidden;
+    }
   }
 
-  const index = inspections.value.findIndex(i => i.id === editForm.value.id)
-  if (index !== -1) {
-    inspections.value[index] = {
-      ...editForm.value,
-      completeTime: editForm.value.isCompleted ? editForm.value.completeTime : null
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  .list-scroll {
+    flex: 1;
+    overflow: hidden;
+  }
+
+  .order-list {
+    padding-right: 4px;
+  }
+
+  .order-item {
+    padding: 16px;
+    margin-bottom: 8px;
+    border-radius: 8px;
+    cursor: pointer;
+    transition: all 0.2s;
+    border: 1px solid #e4e7ed;
+
+    &:hover {
+      background-color: #f5f7fa;
+    }
+
+    &.active {
+      background-color: #ecf5ff;
+      border-color: #409eff;
+    }
+
+    .order-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 8px;
+
+      .order-no {
+        font-weight: bold;
+        font-size: 14px;
+      }
+
+      .status-badge {
+        min-width: 60px;
+      }
+    }
+
+    .order-body {
+      font-size: 12px;
+      color: #606266;
+
+      .order-tags {
+        display: flex;
+        gap: 6px;
+        margin-bottom: 6px;
+      }
+
+      .order-desc {
+        margin-top: 4px;
+        color: #303133;
+        font-size: 13px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+
+      .device-code, .create-time {
+        margin-top: 4px;
+      }
     }
-    alert('巡检记录已更新')
-    closeEditModal()
   }
-}
 
-// ==================== 删除巡检 ====================
-const deleteInspection = (item) => {
-  if (confirm(`确定删除巡检记录吗?`)) {
-    const index = inspections.value.findIndex(i => i.id === item.id)
-    if (index !== -1) {
-      inspections.value.splice(index, 1)
-      alert('巡检记录已删除')
+  .pagination-wrapper {
+    padding: 16px 0;
+    border-top: 1px solid #e4e7ed;
+    margin-top: 8px;
+
+    .pagination {
+      display: flex;
+      justify-content: flex-end;
     }
   }
-}
-</script>
 
-<style scoped>
-* { margin: 0; padding: 0; box-sizing: border-box; }
-
-.prototype-container {
-  max-width: 1600px;
-  margin: 0 auto;
-  background: #f0f4f8;
-  border-radius: 28px;
-  box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.15);
-  overflow: hidden;
-  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
-}
+  .detail-body {
+    flex: 1;
+    overflow-y: auto;
+  }
 
-/* 头部 */
-.header {
-  background: #0a2b3c;
-  padding: 16px 28px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 16px;
-  border-bottom: 3px solid #3498db;
-}
+  .detail-tabs {
+    margin-top: 0;
+  }
 
-.logo h2 { color: white; font-weight: 600; font-size: 1.3rem; }
-.logo p { color: #bdd8e8; font-size: 0.7rem; }
-
-.header-stats { display: flex; gap: 12px; }
-.stat-card { background: rgba(255,255,255,0.15); padding: 6px 14px; border-radius: 40px; text-align: center; color: white; }
-.stat-card.pending { background: #f39c12; }
-.stat-card.processing { background: #3498db; }
-.stat-card.completed { background: #27ae60; }
-.stat-value { font-weight: 700; font-size: 1.2rem; margin-right: 6px; }
-.stat-label { font-size: 0.65rem; opacity: 0.9; }
-
-/* 布局 */
-.main-layout { display: flex; flex-wrap: wrap; }
-.left-panel { width: 400px; background: #f9fbfd; padding: 20px; }
-.right-panel { flex: 1; background: #ffffff; padding: 20px; min-width: 600px; }
-
-/* 卡片 */
-.card { background: white; border-radius: 20px; border: 1px solid #e6edf3; margin-bottom: 20px; overflow: hidden; }
-.card-header { background: #f9fafc; padding: 14px 18px; border-bottom: 1px solid #eef2f6; font-weight: 700; font-size: 0.85rem; color: #1d5a78; display: flex; justify-content: space-between; align-items: center; }
-.card-body { padding: 16px; }
-.card-tip { font-size: 0.65rem; font-weight: normal; color: #6f8ea8; }
-
-/* 表单 */
-.form-group { margin-bottom: 14px; }
-.form-row { display: flex; gap: 12px; margin-bottom: 14px; }
-.form-group.half { flex: 1; }
-.form-label { font-size: 0.7rem; font-weight: 600; margin-bottom: 4px; display: block; color: #2c5a72; }
-.form-input, .form-select, .form-textarea { width: 100%; padding: 8px 12px; border-radius: 12px; border: 1px solid #cbdde6; font-size: 0.75rem; }
-.form-textarea { resize: vertical; }
-.form-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 8px; }
-.btn-primary { background: #3498db; border: none; color: white; padding: 8px 20px; border-radius: 30px; font-size: 0.75rem; cursor: pointer; }
-.btn-primary:hover { background: #2980b9; }
-.btn-outline { background: white; border: 1px solid #3498db; color: #3498db; padding: 6px 16px; border-radius: 30px; font-size: 0.7rem; cursor: pointer; }
-.btn-sm { padding: 4px 12px; font-size: 0.65rem; }
-
-/* 筛选栏 */
-.filter-bar { background: #f8fafc; border-radius: 16px; padding: 12px; margin-bottom: 16px; display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-end; }
-.filter-group { display: flex; flex-direction: column; gap: 4px; }
-.filter-label { font-size: 0.65rem; font-weight: 600; color: #2c5a72; }
-.date-range { display: flex; align-items: center; gap: 6px; }
-.filter-input { padding: 6px 10px; border-radius: 20px; border: 1px solid #cbdde6; font-size: 0.7rem; }
-.filter-select { padding: 6px 10px; border-radius: 20px; border: 1px solid #cbdde6; font-size: 0.7rem; min-width: 100px; }
-.filter-sep { font-size: 0.7rem; color: #6f8ea8; }
-.filter-actions { display: flex; gap: 8px; }
-
-/* 表格 */
-.inspection-table-wrapper { overflow-x: auto; }
-.data-table { width: 100%; border-collapse: collapse; font-size: 0.7rem; }
-.data-table th { background: #f8fafc; padding: 10px 8px; text-align: left; font-weight: 600; color: #2c5a72; border-bottom: 1px solid #e2edf2; }
-.data-table td { padding: 10px 8px; border-bottom: 1px solid #f0f4f7; vertical-align: middle; }
-.time-cell { white-space: nowrap; font-family: monospace; }
-.content-cell { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.issue-cell { max-width: 180px; }
-.issue-badge { display: inline-block; background: #fef3c7; color: #f39c12; padding: 2px 6px; border-radius: 12px; font-size: 0.6rem; margin-right: 6px; }
-.no-issue { display: inline-block; background: #d5f5e3; color: #27ae60; padding: 2px 6px; border-radius: 12px; font-size: 0.6rem; margin-right: 6px; }
-.issue-text { color: #5a6e7e; }
-.process-cell { max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.action-cell { white-space: nowrap; display: flex; gap: 6px; }
-.empty-row { text-align: center; padding: 30px; color: #8aaec2; }
-.list-count { font-size: 0.65rem; font-weight: normal; color: #6f8ea8; }
-
-/* 状态标签 */
-.status-badge { padding: 2px 10px; border-radius: 20px; font-size: 0.65rem; }
-.status-pending { background: #fef3c7; color: #f39c12; }
-.status-processing { background: #e3f2fd; color: #3498db; }
-.status-completed { background: #d5f5e3; color: #27ae60; }
-.status-normal { background: #e8f4fd; color: #3498db; }
-
-/* 操作按钮 */
-.btn-view { background: #95a5a6; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-.btn-process { background: #3498db; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-.btn-edit { background: #f39c12; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-.btn-delete { background: #e74c3c; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-
-/* 分页 */
-.pagination { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
-.page-btn { background: white; border: 1px solid #cbdde6; padding: 4px 10px; border-radius: 20px; font-size: 0.7rem; cursor: pointer; }
-.page-btn.active { background: #3498db; color: white; border-color: #3498db; }
-
-/* 统计区域 */
-.stats-grid { display: flex; gap: 20px; align-items: center; }
-.stat-circle { text-align: center; }
-.circle-num { font-size: 2rem; font-weight: 700; color: #3498db; }
-.circle-label { font-size: 0.65rem; color: #6f8ea8; }
-.stat-list { flex: 1; }
-.stat-row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 0.7rem; border-bottom: 1px solid #eef2f6; }
-.stat-row span:first-child { color: #6f8ea8; }
-.stat-row span:last-child { font-weight: 600; color: #2c5a72; }
-
-/* 模态框 */
-.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
-.modal-content { background: white; border-radius: 24px; width: 500px; max-width: 90%; padding: 24px; max-height: 85vh; overflow-y: auto; }
-.detail-info { background: #f8fafc; border-radius: 16px; padding: 16px; margin-top: 8px; }
-.info-row { margin-bottom: 10px; font-size: 0.75rem; }
-.info-row .label { font-weight: 600; width: 90px; display: inline-block; color: #2c5a72; }
-.process-info-summary { background: #f8fafc; border-radius: 12px; padding: 12px; margin-bottom: 16px; font-size: 0.7rem; }
-.process-info-summary .label { font-weight: 600; }
-</style>
+  .timeline-scroll {
+    max-height: calc(100vh - 340px);
+    overflow-y: auto;
+    padding-right: 8px;
+  }
+
+  .device-section, .image-section, .action-section {
+    margin-top: 24px;
+    padding-top: 24px;
+    border-top: 1px solid #e4e7ed;
+
+    .section-title {
+      font-size: 15px;
+      font-weight: bold;
+      margin-bottom: 16px;
+      color: #303133;
+    }
+  }
+
+  .image-list {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+  }
+
+  .action-buttons {
+    display: flex;
+    gap: 12px;
+    flex-wrap: wrap;
+
+    .el-button {
+      min-width: 100px;
+    }
+  }
+
+  .timeline-content {
+    .timestamp {
+      font-size: 12px;
+      color: #909399;
+      margin-bottom: 6px;
+    }
+
+    .operator-info {
+      font-size: 14px;
+      margin-bottom: 8px;
+
+      .operator-name {
+        font-weight: bold;
+        color: #303133;
+      }
+
+      .oper-type {
+        color: #909399;
+      }
+    }
+
+    .oper-desc {
+      font-size: 14px;
+      color: #606266;
+    }
+  }
+}
+</style>

+ 914 - 577
src/views/subSystem/pipeNetwork/pworkOrder/PworkList.vue

@@ -1,651 +1,988 @@
-<!--工单列表-->
+<!--设备故障工单(故障维修 orderType=1,参考排水/燃气工单形态)-->
+<!--数据源:/api/work-order/*(orderType=1 过滤;equipmentType 为空 = 所有设备)-->
+<!--需求 5.2.4.4.1:手动新增字段=设备类型/设备ID/设备位置/设备厂家/维护人员/联系方式/故障时间/故障描述-->
 <template>
-  <div class="prototype-container">
-    <!-- 头部 -->
-    <div class="header">
-      <div class="logo">
-        <h2>🔧 设备故障工单管理系统</h2>
-        <p>故障自动预警 | 智能派单 | 工单闭环管理</p>
+  <div class="order-container">
+    <!-- 顶部搜索区(参考排水/燃气工单) -->
+    <el-card class="search-card" shadow="never">
+      <div class="search-form">
+        <el-form :model="queryParams" ref="queryFormRef" inline>
+          <el-form-item label="优先级">
+            <el-select v-model="queryParams.orderLevel" placeholder="全部优先级" clearable style="width: 120px">
+              <el-option v-for="(v, k) in ORDER_LEVEL_MAP" :key="k" :label="v.text" :value="Number(k)"/>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="状态">
+            <el-select v-model="queryParams.orderStatus" placeholder="全部状态" clearable style="width: 120px">
+              <el-option v-for="(v, k) in ORDER_STATUS_MAP" :key="k" :label="v.text" :value="Number(k)"/>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="设备编码">
+            <el-input v-model="queryParams.deviceCode" placeholder="设备编码" clearable style="width: 150px" @keyup.enter="handleSearch"/>
+          </el-form-item>
+          <el-form-item label="关键词">
+            <el-input v-model="queryParams.keyword" placeholder="工单编号/设备编码/描述" clearable style="width: 200px" @keyup.enter="handleSearch"/>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="handleSearch">查询</el-button>
+            <el-button @click="handleReset">重置</el-button>
+            <el-button type="primary" plain @click="openAddDialog">➕ 新增故障工单</el-button>
+          </el-form-item>
+        </el-form>
       </div>
-      <div class="header-stats">
-        <div class="stat-card pending">
-          <span class="stat-value">{{ pendingCount }}</span>
-          <span class="stat-label">待处理</span>
-        </div>
-        <div class="stat-card processing">
-          <span class="stat-value">{{ processingCount }}</span>
-          <span class="stat-label">处理中</span>
-        </div>
-        <div class="stat-card completed">
-          <span class="stat-value">{{ completedCount }}</span>
-          <span class="stat-label">已完成</span>
-        </div>
-      </div>
-    </div>
-
-    <div class="main-layout">
-      <!-- 左侧:实时故障监控与自动生成 -->
-      <div class="left-panel">
-        <div class="card">
-          <div class="card-header">
-            🚨 实时故障监控
-            <span class="live-badge">● 自动监测中</span>
-          </div>
-          <div class="card-body">
-            <div class="fault-monitor">
-              <div class="monitor-list">
-                <div v-for="fault in recentFaults" :key="fault.id" class="fault-item" :class="{ new: fault.isNew }">
-                  <div class="fault-time">{{ fault.time }}</div>
-                  <div class="fault-device">{{ fault.deviceName }}</div>
-                  <div class="fault-message">{{ fault.message }}</div>
-                  <div class="fault-order-status">
-                    <span v-if="fault.orderCreated" class="order-created">✅ 工单已生成</span>
-                    <span v-else class="order-creating">⏳ 正在生成工单...</span>
-                  </div>
-                </div>
-                <div v-if="recentFaults.length === 0" class="empty-fault">
-                  暂无实时故障,系统运行正常
-                </div>
-              </div>
+    </el-card>
+
+    <!-- 主容器:左列表 + 右详情(参考排水/燃气工单) -->
+    <el-row :gutter="16" class="main-row">
+      <!-- 左侧:工单列表 -->
+      <el-col :span="8" class="col-fill">
+        <el-card class="box-card" shadow="always">
+          <template #header>
+            <div class="card-header">
+              <span>故障工单列表</span>
             </div>
-          </div>
-        </div>
-
-        <!-- 手动新增工单 -->
-        <div class="card">
-          <div class="card-header">
-            ➕ 手动新增故障工单
-            <button @click="openAddModal" class="btn-primary btn-sm">+ 新增工单</button>
-          </div>
-          <div class="card-body" v-if="showAddForm">
-            <div class="add-form">
-              <div class="form-row">
-                <div class="form-group half">
-                  <label class="form-label">设备类型 *</label>
-                  <select v-model="newOrder.deviceType" class="form-select">
-                    <option value="">请选择</option>
-                    <option value="压力传感器">压力传感器</option>
-                    <option value="流量传感器">流量传感器</option>
-                    <option value="温度传感器">温度传感器</option>
-                    <option value="泄漏检测仪">泄漏检测仪</option>
-                    <option value="水位监测仪">水位监测仪</option>
-                  </select>
-                </div>
-                <div class="form-group half">
-                  <label class="form-label">设备ID *</label>
-                  <input v-model="newOrder.deviceId" class="form-input" placeholder="如: PS-001">
-                </div>
-              </div>
-              <div class="form-row">
-                <div class="form-group half">
-                  <label class="form-label">设备位置 *</label>
-                  <input v-model="newOrder.location" class="form-input" placeholder="设备安装位置">
+          </template>
+          <el-scrollbar class="list-scroll">
+            <div class="order-list">
+              <div
+                v-for="order in tableData"
+                :key="order.orderId"
+                class="order-item"
+                :class="{ 'active': selectedOrderId === order.orderId }"
+                @click="handleSelectOrder(order)"
+              >
+                <div class="order-header">
+                  <span class="order-no">{{ order.orderNo }}</span>
+                  <el-tag :type="orderStatusType(order.orderStatus)" size="small" effect="light" class="status-badge">{{ orderStatusText(order.orderStatus) }}</el-tag>
                 </div>
-                <div class="form-group half">
-                  <label class="form-label">设备厂家</label>
-                  <input v-model="newOrder.manufacturer" class="form-input" placeholder="设备生产厂家">
-                </div>
-              </div>
-              <div class="form-row">
-                <div class="form-group half">
-                  <label class="form-label">维护人员 *</label>
-                  <select v-model="newOrder.maintainer" class="form-select">
-                    <option value="">请选择</option>
-                    <option value="张工">张工(设备组)</option>
-                    <option value="李工">李工(运维组)</option>
-                    <option value="王工">王工(抢修组)</option>
-                    <option value="赵工">赵工(技术组)</option>
-                  </select>
-                </div>
-                <div class="form-group half">
-                  <label class="form-label">联系方式</label>
-                  <input v-model="newOrder.contact" class="form-input" placeholder="维护人员电话">
+                <div class="order-body">
+                  <div class="order-tags">
+                    <el-tag :type="orderLevelType(order.orderLevel)" size="small" effect="plain">{{ orderLevelText(order.orderLevel) }}</el-tag>
+                    <el-tag v-if="order.hazardNo" size="small" effect="plain">隐患 {{ order.hazardNo }}</el-tag>
+                  </div>
+                  <div class="order-desc" v-if="order.orderDesc">{{ order.orderDesc }}</div>
+                  <div class="device-code">设备编号:{{ order.deviceCode || '-' }}</div>
+                  <div class="create-time">故障时间:{{ timeText(order.createTime) }}</div>
                 </div>
               </div>
-              <div class="form-group">
-                <label class="form-label">故障时间 *</label>
-                <input type="datetime-local" v-model="newOrder.faultTime" class="form-input">
-              </div>
-              <div class="form-group">
-                <label class="form-label">故障描述 *</label>
-                <textarea v-model="newOrder.description" class="form-textarea" rows="2" placeholder="请描述故障现象..."></textarea>
-              </div>
-              <div class="form-actions">
-                <button @click="cancelAdd" class="btn-outline">取消</button>
-                <button @click="submitManualOrder" class="btn-primary">提交工单</button>
-              </div>
+              <el-empty v-if="tableData.length === 0" description="暂无数据" :image-size="100"/>
             </div>
+          </el-scrollbar>
+          <div class="pagination-wrapper">
+            <el-pagination
+              v-model:current-page="pageNum"
+              v-model:page-size="pageSize"
+              :total="total"
+              layout="total, prev, pager, next"
+              :page-sizes="[10, 20, 50, 100]"
+              @size-change="handleSizeChange"
+              @current-change="handlePageChange"
+              class="pagination"
+            />
           </div>
-          <div class="card-body" v-else>
-            <div class="empty-add" @click="openAddModal">
-              <span class="add-icon">+</span>
-              <span>点击新增故障工单</span>
+        </el-card>
+      </el-col>
+
+      <!-- 右侧:工单详情 -->
+      <el-col :span="16" class="col-fill">
+        <el-card v-loading="detailLoading" class="box-card" shadow="always">
+          <template #header>
+            <div class="card-header">
+              <span v-if="selectedOrder && selectedOrder.orderId">工单详情 - {{ selectedOrder.orderNo }}</span>
+              <span v-else>请选择工单查看详情</span>
             </div>
-          </div>
-        </div>
-      </div>
+          </template>
+
+          <div v-if="selectedOrder && selectedOrder.orderId" class="detail-body">
+            <el-tabs v-model="activeTab" class="detail-tabs">
+              <!-- 工单信息 -->
+              <el-tab-pane label="工单信息" name="info">
+                <el-descriptions :column="2" border size="small">
+                  <el-descriptions-item label="工单编号">{{ selectedOrder.orderNo }}</el-descriptions-item>
+                  <el-descriptions-item label="工单状态">
+                    <el-tag :type="orderStatusType(selectedOrder.orderStatus)" size="small">{{ orderStatusText(selectedOrder.orderStatus) }}</el-tag>
+                  </el-descriptions-item>
+                  <el-descriptions-item label="关联案件编号">{{ selectedOrder.caseNo || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="关联隐患编号">{{ selectedOrder.hazardNo || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="设备编号">{{ selectedOrder.deviceCode || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="工单类型">{{ orderTypeText(selectedOrder.orderType) }}</el-descriptions-item>
+                  <el-descriptions-item label="优先级">
+                    <el-tag :type="orderLevelType(selectedOrder.orderLevel)" size="small">{{ orderLevelText(selectedOrder.orderLevel) }}</el-tag>
+                  </el-descriptions-item>
+                  <el-descriptions-item label="接单人">{{ userNames(selectedOrder.receiveUser) || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="问题描述" :span="2">{{ selectedOrder.orderDesc || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="故障时间">{{ timeText(selectedOrder.createTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="派单时间">{{ timeText(selectedOrder.dispatchTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="计划完成">{{ timeText(selectedOrder.planFinishTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="实际完成">{{ timeText(selectedOrder.realFinishTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="验收时间">{{ timeText(selectedOrder.verifyTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="创建时间">{{ timeText(selectedOrder.createTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="更新时间">{{ timeText(selectedOrder.updateTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="处置结果" :span="2">{{ selectedOrder.repairResult || '-' }}</el-descriptions-item>
+                </el-descriptions>
+
+                <!-- 关联设备信息(需求 5.2.4.4.1 设备维度) -->
+                <div v-if="detailDevice" class="device-section">
+                  <h4 class="section-title">🔗 关联设备</h4>
+                  <el-descriptions :column="2" border size="small">
+                    <el-descriptions-item label="设备类型">{{ detailDevice.equipmentTypeName || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="设备编码">{{ detailDevice.equipmentCode || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="安装位置">{{ detailDevice.equipmentLocation || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="管点地址">{{ detailDevice.pointAddress || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="设备厂家">{{ detailDevice.manufacturer || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="所属管线">{{ detailDevice.networkName || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="维护人员">{{ detailDevice.maintainer || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="联系方式">{{ detailDevice.maintainerPhone || '-' }}</el-descriptions-item>
+                  </el-descriptions>
+                </div>
 
-      <!-- 右侧:工单列表 -->
-      <div class="right-panel">
-        <div class="card">
-          <div class="card-header">
-            📋 设备故障工单列表
-            <span class="list-count">共 {{ filteredOrders.length }} 条记录</span>
+                <RepairImagePanel :before-images="selectedOrder.maintenanceBeforeImg" :after-images="selectedOrder.maintenanceAfterImg" />
+
+                <!-- 工单操作(参考排水/燃气工单按钮组) -->
+                <div class="action-section">
+                  <h4 class="section-title">工单操作</h4>
+                  <div class="action-buttons">
+                    <el-button v-if="canOperate.dispatch(selectedOrder)" type="primary" @click="handleDispatch">下派任务</el-button>
+                    <el-button v-if="canOperate.verify(selectedOrder)" type="warning" @click="handleVerify">核查</el-button>
+                    <el-button v-if="canOperate.process(selectedOrder)" type="success" @click="handleProcess">处理</el-button>
+                    <el-button v-if="canOperate.complete(selectedOrder)" type="danger" @click="handleComplete">提交验收</el-button>
+                    <el-button v-if="canOperate.close(selectedOrder)" type="success" @click="handleClose">验收通过</el-button>
+                    <el-button v-if="canOperate.cancel(selectedOrder)" type="info" plain @click="handleCancel">取消工单</el-button>
+                  </div>
+                </div>
+              </el-tab-pane>
+
+              <!-- 处理历史 -->
+              <el-tab-pane label="处理历史" name="history">
+                <div class="timeline-scroll">
+                  <el-timeline v-if="currentOrderLogs && currentOrderLogs.length > 0">
+                    <el-timeline-item
+                      v-for="log in currentOrderLogs"
+                      :key="log.logId || log.id"
+                      :timestamp="timeText(log.operTime)"
+                      placement="top"
+                      :type="orderLogType(log.operType)"
+                      size="large"
+                    >
+                      <el-card shadow="hover">
+                        <div class="timeline-content">
+                          <div class="timestamp">{{ timeText(log.operTime) }}</div>
+                          <div class="operator-info">
+                            <span class="operator-name">{{ log.operUserName || '-' }}</span>
+                            <span class="oper-type">· {{ orderLogText(log.operType) }}</span>
+                          </div>
+                          <div class="oper-desc">{{ log.operDesc || '-' }}</div>
+                        </div>
+                      </el-card>
+                    </el-timeline-item>
+                  </el-timeline>
+                  <el-empty v-else description="暂无处理记录" :image-size="100"/>
+                </div>
+              </el-tab-pane>
+            </el-tabs>
           </div>
-          <div class="card-body">
-            <!-- 查询筛选栏 -->
-            <div class="filter-bar">
-              <select v-model="queryParams.deviceType" class="filter-select">
-                <option value="">设备类型</option>
-                <option value="压力传感器">压力传感器</option>
-                <option value="流量传感器">流量传感器</option>
-                <option value="温度传感器">温度传感器</option>
-                <option value="泄漏检测仪">泄漏检测仪</option>
-              </select>
-              <input type="text" v-model="queryParams.deviceId" class="filter-input" placeholder="设备ID">
-              <input type="date" v-model="queryParams.startDate" class="filter-input" placeholder="开始日期">
-              <span class="filter-sep">至</span>
-              <input type="date" v-model="queryParams.endDate" class="filter-input" placeholder="结束日期">
-              <button @click="searchOrders" class="btn-primary btn-sm">查询</button>
-              <button @click="resetQuery" class="btn-outline btn-sm">重置</button>
-            </div>
 
-            <!-- 工单表格 -->
-            <div class="order-table-wrapper">
-              <table class="data-table">
-                <thead>
-                <tr>
-                  <th>工单编号</th>
-                  <th>设备类型</th>
-                  <th>设备ID</th>
-                  <th>设备位置</th>
-                  <th>故障描述</th>
-                  <th>故障时间</th>
-                  <th>维护人员</th>
-                  <th>状态</th>
-                  <th>操作</th>
-                </tr>
-                </thead>
-                <tbody>
-                <tr v-for="order in paginatedOrders" :key="order.id">
-                  <td class="order-no">{{ order.orderNo }}</td>
-                  <td>{{ order.deviceType }}</td>
-                  <td>{{ order.deviceId }}</td>
-                  <td>{{ order.location }}</td>
-                  <td class="desc-cell">{{ order.description }}</td>
-                  <td class="time-cell">{{ order.faultTime }}</td>
-                  <td>{{ order.maintainer }}</td>
-                  <td>
-                      <span class="status-badge" :class="getStatusClass(order.status)">
-                        {{ getStatusText(order.status) }}
-                      </span>
-                  </td>
-                  <td class="action-cell">
-                    <button @click="viewOrderDetail(order)" class="btn-view">查看</button>
-                    <button v-if="order.status === 'pending'" @click="updateStatus(order, 'processing')" class="btn-start">开始处理</button>
-                    <button v-if="order.status === 'processing'" @click="completeOrder(order)" class="btn-complete">完成</button>
-                  </td>
-                </tr>
-                <tr v-if="filteredOrders.length === 0">
-                  <td colspan="9" class="empty-row">暂无工单数据</td>
-                </tr>
-                </tbody>
-              </table>
-            </div>
-
-            <!-- 分页 -->
-            <div class="pagination" v-if="totalPages > 1">
-              <button v-for="page in totalPages" :key="page" class="page-btn" :class="{ active: currentPage === page }"
-                      @click="currentPage = page">{{ page }}</button>
-            </div>
+          <el-empty v-else description="请从左侧选择工单" :image-size="150"/>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 新增工单弹窗(需求 5.2.4.4.1:设备类型/设备ID/设备位置/设备厂家/维护人员/联系方式/故障时间/故障描述) -->
+    <el-dialog v-model="addVisible" title="➕ 新增故障工单" width="680px" append-to-body destroy-on-close>
+      <el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="100px">
+        <el-form-item label="关联设备" prop="deviceId">
+          <el-select v-model="addForm.deviceId" filterable remote clearable placeholder="输入设备编码/名称搜索" :remote-method="searchDevice" :loading="deviceLoading" style="width:100%" @change="onDeviceChange">
+            <el-option v-for="dev in deviceOptions" :key="dev.equipmentId" :label="`${dev.equipmentName || dev.equipmentCode} (${dev.equipmentCode})`" :value="dev.equipmentId"/>
+          </el-select>
+        </el-form-item>
+
+        <!-- 设备信息卡:选择设备后自动带出 设备类型/设备ID/设备位置/设备厂家/维护人员/联系方式 -->
+        <el-form-item v-if="deviceInfo" label="设备信息">
+          <div class="device-info-card">
+            <el-descriptions :column="2" border size="small">
+              <el-descriptions-item label="设备类型">{{ deviceInfo.equipmentTypeName || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="设备ID">{{ deviceInfo.equipmentCode || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="设备位置">{{ deviceInfo.equipmentLocation || deviceInfo.pointAddress || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="设备厂家">{{ deviceInfo.manufacturer || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="维护人员">{{ deviceInfo.maintainer || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="联系方式">{{ deviceInfo.maintainerPhone || '-' }}</el-descriptions-item>
+            </el-descriptions>
           </div>
+        </el-form-item>
+
+        <el-form-item label="设备编码">
+          <el-input v-model="addForm.deviceCode" placeholder="未选设备时手动填写" clearable/>
+        </el-form-item>
+        <el-form-item label="优先级" prop="orderLevel">
+          <el-radio-group v-model="addForm.orderLevel">
+            <el-radio-button :value="1">紧急</el-radio-button>
+            <el-radio-button :value="2">一般</el-radio-button>
+            <el-radio-button :value="3">低</el-radio-button>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="故障时间">
+          <el-date-picker v-model="addForm.faultTime" type="datetime" placeholder="故障发生时间(默认当前)" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/>
+        </el-form-item>
+        <el-form-item label="计划完成时间">
+          <el-date-picker v-model="addForm.planFinishTime" type="datetime" placeholder="请选择计划完成时间" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/>
+        </el-form-item>
+        <el-form-item label="故障描述" prop="orderDesc">
+          <el-input v-model="addForm.orderDesc" type="textarea" :rows="4" placeholder="请描述故障现象 / 设备离线信息等" maxlength="500" show-word-limit/>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="addVisible=false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitAdd">提交工单</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 派单弹窗(部门树 + 用户多选,参考排水/燃气工单) -->
+    <el-dialog v-model="dispatchDialogVisible" title="下派任务 - 选择接单人" width="750px" append-to-body>
+      <div style="display: flex; gap: 16px; min-height: 350px;">
+        <div style="width: 280px; border-right: 1px solid #e4e7ed; padding-right: 12px;">
+          <el-input v-model="deptFilterText" placeholder="筛选部门" clearable size="small" style="margin-bottom: 8px"/>
+          <el-tree
+            ref="deptTreeRef"
+            :data="deptTreeData"
+            :props="{ label: 'deptName', children: 'children' }"
+            :filter-node-method="filterDeptNode"
+            node-key="deptId"
+            highlight-current
+            @node-click="handleDeptClick"
+            style="font-size: 14px"
+          />
         </div>
-      </div>
-    </div>
-
-    <!-- 工单详情模态框 -->
-    <div v-if="detailModalVisible" class="modal-overlay" @click.self="closeDetailModal">
-      <div class="modal-content">
-        <h3>📋 工单详细信息</h3>
-        <div class="detail-info">
-          <div class="info-row"><span class="label">工单编号:</span>{{ currentOrder?.orderNo }}</div>
-          <div class="info-row"><span class="label">设备类型:</span>{{ currentOrder?.deviceType }}</div>
-          <div class="info-row"><span class="label">设备ID:</span>{{ currentOrder?.deviceId }}</div>
-          <div class="info-row"><span class="label">设备位置:</span>{{ currentOrder?.location }}</div>
-          <div class="info-row"><span class="label">设备厂家:</span>{{ currentOrder?.manufacturer || '-' }}</div>
-          <div class="info-row"><span class="label">维护人员:</span>{{ currentOrder?.maintainer }}</div>
-          <div class="info-row"><span class="label">联系方式:</span>{{ currentOrder?.contact || '-' }}</div>
-          <div class="info-row"><span class="label">故障时间:</span>{{ currentOrder?.faultTime }}</div>
-          <div class="info-row"><span class="label">故障描述:</span>{{ currentOrder?.description }}</div>
-          <div class="info-row"><span class="label">创建时间:</span>{{ currentOrder?.createTime }}</div>
-          <div class="info-row" v-if="currentOrder?.startTime"><span class="label">开始处理:</span>{{ currentOrder?.startTime }}</div>
-          <div class="info-row" v-if="currentOrder?.completeTime"><span class="label">完成时间:</span>{{ currentOrder?.completeTime }}</div>
-          <div class="info-row" v-if="currentOrder?.processResult"><span class="label">处理结果:</span>{{ currentOrder?.processResult }}</div>
-        </div>
-        <div class="form-actions">
-          <button @click="closeDetailModal" class="btn-primary">关闭</button>
+        <div style="flex: 1; min-width: 0;">
+          <el-table
+            ref="userTableRef"
+            :data="userList"
+            border size="small"
+            max-height="300"
+            row-key="userId"
+            @selection-change="handleSelectionChange"
+            @row-click="handleRowClick"
+          >
+            <el-table-column type="selection" width="40" align="center" :reserve-selection="true"/>
+            <el-table-column prop="userName" label="用户名" width="100"/>
+            <el-table-column prop="nickName" label="昵称" width="100"/>
+            <el-table-column prop="dept.deptName" label="部门" show-overflow-tooltip/>
+          </el-table>
+          <div v-if="userList.length === 0" style="text-align: center; color: #909399; padding: 40px 0;">请先在左侧选择部门</div>
         </div>
       </div>
-    </div>
-
-    <!-- 完成工单模态框 -->
-    <div v-if="completeModalVisible" class="modal-overlay" @click.self="closeCompleteModal">
-      <div class="modal-content" style="width:450px;">
-        <h3>✅ 工单完成反馈</h3>
-        <div class="order-info-summary">
-          <div><span class="label">工单号:</span>{{ currentOrder?.orderNo }}</div>
-          <div><span class="label">设备:</span>{{ currentOrder?.deviceId }} - {{ currentOrder?.deviceType }}</div>
-        </div>
-        <div class="form-group">
-          <label class="form-label">处理结果</label>
-          <textarea v-model="processResult" class="form-textarea" rows="3" placeholder="请描述处理过程及结果..."></textarea>
-        </div>
-        <div class="form-actions">
-          <button @click="closeCompleteModal" class="btn-outline">取消</button>
-          <button @click="submitComplete" class="btn-primary">提交完成</button>
-        </div>
-      </div>
-    </div>
+      <el-form label-width="80px" style="margin-top: 16px">
+        <el-form-item label="备注"><el-input v-model="dispatchForm.remarks" placeholder="请输入派单备注"/></el-form-item>
+      </el-form>
+      <template #footer>
+        <span style="float: left; line-height: 32px; color: #606266;">
+          已选人员:
+          <span v-if="selectedUserNames.length > 0" style="color: #409eff; font-weight: bold;">{{ selectedUserNames.join('、') }}</span>
+          <span v-else style="color: #909399">未选择</span>
+          ({{ dispatchForm.handlerIds.length }})
+        </span>
+        <el-button @click="dispatchDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitDispatch">确认</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 核查/处理弹窗 -->
+    <el-dialog v-model="actionDialogVisible" :title="actionTitle" width="600px" append-to-body>
+      <el-form label-width="90px">
+        <el-form-item v-if="actionTitle === '处理工单'" label="处理前照片">
+          <ImageUpload v-model="processBeforeImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
+        </el-form-item>
+        <el-form-item label="结果说明" required>
+          <el-input v-model="actionFormData.solution" type="textarea" :rows="5" :placeholder="actionTitle === '核查工单' ? '请输入核查结论(属实/不属实、现场情况)' : '请输入故障原因、处理结果及处理措施'" maxlength="500" show-word-limit/>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="actionDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitAction">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 提交验收弹窗 -->
+    <el-dialog v-model="completeDialogVisible" title="提交验收" width="600px" append-to-body>
+      <el-form label-width="80px">
+        <el-form-item label="维修后照片">
+          <ImageUpload v-model="maintenanceAfterImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
+        </el-form-item>
+        <el-form-item label="验收意见">
+          <el-input v-model="completeForm.result" type="textarea" :rows="4" placeholder="请填写维修处置结果说明" maxlength="500" show-word-limit/>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="completeDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitComplete">提交</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 验收通过/结案弹窗 -->
+    <el-dialog v-model="closeDialogVisible" title="验收通过" width="500px" append-to-body>
+      <el-form label-width="80px">
+        <el-form-item label="验收结果">
+          <el-input v-model="closeForm.result" type="textarea" :rows="4" placeholder="请填写验收结果(结案后关联隐患同步为已整改、设备报警状态重置)" maxlength="500" show-word-limit/>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="closeDialogVisible = false">取消</el-button>
+        <el-button type="success" :loading="submitting" @click="submitClose">确认验收</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onUnmounted } from 'vue'
-
-// ==================== 工单数据 ====================
-const orders = ref([
-  { id: 1, orderNo: 'WO-20250508-001', deviceType: '压力传感器', deviceId: 'PS-001', location: '城东泵站', manufacturer: '华仪科技', maintainer: '张工', contact: '13800001111', faultTime: '2025-05-08 09:23:15', description: '设备离线,数据上报异常,超过30分钟未回传', createTime: '2025-05-08 09:23:18', status: 'pending', startTime: null, completeTime: null, processResult: null },
-  { id: 2, orderNo: 'WO-20250508-002', deviceType: '泄漏检测仪', deviceId: 'LD-005', location: '化工园区', manufacturer: '安防科技', maintainer: '李工', contact: '13800002222', faultTime: '2025-05-08 07:30:22', description: '可燃气浓度超阈值报警,需现场核查', createTime: '2025-05-08 07:30:25', status: 'processing', startTime: '2025-05-08 08:15:00', completeTime: null, processResult: null },
-  { id: 3, orderNo: 'WO-20250507-003', deviceType: '水位监测仪', deviceId: 'WL-006', location: '东区排水口', manufacturer: '水文科技', maintainer: '王工', contact: '13800003333', faultTime: '2025-05-07 14:20:33', description: '水位数据异常波动,需校准', createTime: '2025-05-07 14:20:36', status: 'completed', startTime: '2025-05-07 14:35:00', completeTime: '2025-05-07 15:20:00', processResult: '已校准传感器,数据恢复正常' },
-  { id: 4, orderNo: 'WO-20250508-004', deviceType: '流量传感器', deviceId: 'FL-002', location: '城南枢纽', manufacturer: '水测智能', maintainer: '张工', contact: '13800001111', faultTime: '2025-05-08 06:15:00', description: '流量数据中断,设备可能离线', createTime: '2025-05-08 06:15:03', status: 'pending', startTime: null, completeTime: null, processResult: null }
-])
-
-// 实时故障监控模拟数据
-const recentFaults = ref([
-  { id: 101, time: '09:23:15', deviceName: '压力传感器 PS-001', message: '设备离线,数据上报异常', orderCreated: true, isNew: false },
-  { id: 102, time: '07:30:22', deviceName: '泄漏检测仪 LD-005', message: '可燃气浓度超阈值报警', orderCreated: true, isNew: false },
-  { id: 103, time: '06:15:00', deviceName: '流量传感器 FL-002', message: '流量数据中断', orderCreated: true, isNew: false }
-])
-
-// ==================== 查询参数 ====================
-const queryParams = ref({
-  deviceType: '',
-  deviceId: '',
-  startDate: '',
-  endDate: ''
-})
-const currentPage = ref(1)
-const pageSize = 8
-
-// 过滤后的工单(时间倒序)
-const filteredOrders = computed(() => {
-  let result = [...orders.value]
-  if (queryParams.value.deviceType) {
-    result = result.filter(o => o.deviceType === queryParams.value.deviceType)
-  }
-  if (queryParams.value.deviceId) {
-    result = result.filter(o => o.deviceId.toLowerCase().includes(queryParams.value.deviceId.toLowerCase()))
-  }
-  if (queryParams.value.startDate) {
-    result = result.filter(o => o.faultTime >= queryParams.value.startDate)
-  }
-  if (queryParams.value.endDate) {
-    result = result.filter(o => o.faultTime <= queryParams.value.endDate + ' 23:59:59')
-  }
-  // 时间倒序排列
-  return result.sort((a, b) => new Date(b.faultTime) - new Date(a.faultTime))
-})
+import { ref, reactive, computed, watch, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import RepairImagePanel from '@/components/RepairImagePanel'
+import { listWorkOrder, getWorkOrderDetail, addWorkOrder, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder, cancelWorkOrder } from '@/api/work/order'
+import { getEquipmentFullPage } from '@/api/pipeNetwork/basic'
+import { listDept } from '@/api/system/dept'
+import { listUser } from '@/api/system/user'
+import { handleTree } from '@/utils/ruoyi'
+import { ORDER_STATUS_MAP, ORDER_LEVEL_MAP, orderStatusText, orderStatusType, orderTypeText, orderLevelText, orderLevelType, orderLogText, orderLogType, canOperate, timeText } from './workOrderDict'
+
+// ========== 查询 & 列表(orderType=1 故障维修) ==========
+const listLoading = ref(false)
+const tableData = ref([])
+const pageNum = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const queryFormRef = ref(null)
+const queryParams = reactive({ orderLevel: null, orderStatus: null, deviceCode: '', keyword: '' })
+
+async function handleSearch() {
+  listLoading.value = true
+  try {
+    const params = { orderType: 1 }
+    if (queryParams.orderLevel) params.orderLevel = queryParams.orderLevel
+    if (queryParams.orderStatus) params.orderStatus = queryParams.orderStatus
+    if (queryParams.deviceCode) params.deviceCode = queryParams.deviceCode
+    if (queryParams.keyword) params.keyword = queryParams.keyword
+    const res = await listWorkOrder(params, pageNum.value, pageSize.value)
+    if (res.data) {
+      tableData.value = res.data.records || []
+      total.value = Number(res.data.total || 0)
+    }
+  } catch(e) { ElMessage.error('查询失败'); console.error(e) }
+  finally { listLoading.value = false }
+}
 
-const paginatedOrders = computed(() => {
-  const start = (currentPage.value - 1) * pageSize
-  return filteredOrders.value.slice(start, start + pageSize)
-})
+function handleReset() {
+  queryFormRef.value && queryFormRef.value.resetFields()
+  queryParams.orderLevel = null
+  queryParams.orderStatus = null
+  queryParams.deviceCode = ''
+  queryParams.keyword = ''
+  pageNum.value = 1
+  selectedOrderId.value = null
+  selectedOrder.value = {}
+  handleSearch()
+}
 
-const totalPages = computed(() => Math.ceil(filteredOrders.value.length / pageSize))
+// ========== 选中工单 & 详情(合并 res.data.order + logs) ==========
+const selectedOrderId = ref(null)
+const selectedOrder = ref({})
+const activeTab = ref('info')
+const currentOrderLogs = ref([])
+const detailLoading = ref(false)
+const detailDevice = ref(null)
+
+async function handleSelectOrder(order) {
+  selectedOrderId.value = order.orderId
+  selectedOrder.value = { ...order }
+  activeTab.value = 'info'
+  currentOrderLogs.value = []
+  detailDevice.value = null
+  await loadOrderDetail(order.orderId)
+}
 
-// 统计数据
-const pendingCount = computed(() => orders.value.filter(o => o.status === 'pending').length)
-const processingCount = computed(() => orders.value.filter(o => o.status === 'processing').length)
-const completedCount = computed(() => orders.value.filter(o => o.status === 'completed').length)
+async function loadOrderDetail(orderId) {
+  detailLoading.value = true
+  try {
+    const res = await getWorkOrderDetail(orderId)
+    if (res.code === 200 && res.data) {
+      selectedOrder.value = { ...selectedOrder.value, ...res.data.order }
+      currentOrderLogs.value = res.data.logs || []
+      if (res.data.order?.deviceCode) {
+        loadDeviceInfo(res.data.order.deviceCode)
+      }
+    }
+  } catch(e) { ElMessage.error('加载工单详情失败'); console.error(e) }
+  finally { detailLoading.value = false }
+}
 
-// ==================== 辅助函数 ====================
-const getStatusText = (status) => {
-  const map = { pending: '待处理', processing: '处理中', completed: '已完成' }
-  return map[status] || status
+async function loadDeviceInfo(codeOrId) {
+  try {
+    const res = await getEquipmentFullPage(1, 1, { equipmentCode: codeOrId })
+    const rows = res.data?.rows || res.data?.records || []
+    if (rows.length) { detailDevice.value = rows[0]; return }
+    const res2 = await getEquipmentFullPage(1, 1, { equipmentId: codeOrId })
+    const rows2 = res2.data?.rows || res2.data?.records || []
+    if (rows2.length) detailDevice.value = rows2[0]
+  } catch(e) { console.warn('加载设备信息失败', e) }
 }
 
-const getStatusClass = (status) => {
-  const map = { pending: 'status-pending', processing: 'status-processing', completed: 'status-completed' }
-  return map[status] || ''
+function handleSizeChange() {
+  pageNum.value = 1
+  selectedOrderId.value = null
+  selectedOrder.value = {}
+  handleSearch()
 }
 
-// 生成工单编号
-const generateOrderNo = () => {
-  const today = new Date()
-  const dateStr = today.toISOString().slice(0, 10).replace(/-/g, '')
-  const count = orders.value.length + 1
-  return `WO-${dateStr}-${String(count).padStart(3, '0')}`
+function handlePageChange() {
+  selectedOrderId.value = null
+  selectedOrder.value = {}
+  handleSearch()
 }
 
-// ==================== 自动生成故障工单 ====================
-let faultInterval = null
-let faultIdCounter = 200
-
-// 模拟接收设备故障并自动生成工单
-const simulateFaultReceive = () => {
-  if (Math.random() < 0.02) { // 约5%概率触发新故障
-    const faultsList = [
-      { deviceType: '压力传感器', deviceId: 'PS-008', location: '北环加压站', manufacturer: '华仪科技', message: '压力波动异常,超出预警范围' },
-      { deviceType: '温度传感器', deviceId: 'TP-010', location: '城西换热站', manufacturer: '北方仪表', message: '温度超过85℃警戒线' },
-      { deviceType: '泄漏检测仪', deviceId: 'LD-012', location: '南站调压站', manufacturer: '安防科技', message: '可燃气浓度异常波动' }
-    ]
-    const randomFault = faultsList[Math.floor(Math.random() * faultsList.length)]
-    const now = new Date()
-    const timeStr = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`
-    const timeDisplay = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`
-
-    // 添加到实时监控
-    const newFault = {
-      id: faultIdCounter++,
-      time: timeDisplay,
-      deviceName: `${randomFault.deviceType} ${randomFault.deviceId}`,
-      message: randomFault.message,
-      orderCreated: false,
-      isNew: true
+// ========== 用户映射(接单人 ID → 姓名) ==========
+const userMap = ref({})
+function userNames(ids) {
+  if (!ids) return ''
+  return String(ids).split(',').map(id => userMap.value[id] || id).join('、')
+}
+async function loadUsers() {
+  try {
+    const res = await listUser({ pageNum: 1, pageSize: 200 })
+    if (res.rows) {
+      userMap.value = Object.fromEntries(res.rows.map(u => [u.userId, u.nickName || u.userName]))
     }
-    recentFaults.value.unshift(newFault)
-    if (recentFaults.value.length > 10) recentFaults.value.pop()
-
-    // 自动生成工单
-    setTimeout(() => {
-      createFaultOrder(randomFault.deviceType, randomFault.deviceId, randomFault.location, randomFault.manufacturer, randomFault.message, timeStr)
-      newFault.orderCreated = true
-      newFault.isNew = false
-    }, 1500)
-  }
+  } catch(e) { console.warn('加载用户列表失败', e) }
 }
 
-// 创建故障工单并自动派发
-const createFaultOrder = (deviceType, deviceId, location, manufacturer, message, faultTime) => {
-  // 根据设备类型自动分配维护人员
-  const maintainerMap = {
-    '压力传感器': '张工',
-    '温度传感器': '李工',
-    '泄漏检测仪': '王工',
-    '流量传感器': '张工',
-    '水位监测仪': '赵工'
-  }
-  const maintainer = maintainerMap[deviceType] || '张工'
-  const contactMap = { '张工': '13800001111', '李工': '13800002222', '王工': '13800003333', '赵工': '13800004444' }
-
-  const newOrder = {
-    id: orders.value.length + 1,
-    orderNo: generateOrderNo(),
-    deviceType: deviceType,
-    deviceId: deviceId,
-    location: location,
-    manufacturer: manufacturer,
-    maintainer: maintainer,
-    contact: contactMap[maintainer],
-    faultTime: faultTime,
-    description: message,
-    createTime: new Date().toLocaleString(),
-    status: 'pending',
-    startTime: null,
-    completeTime: null,
-    processResult: null
+// ========== 新增工单(需求 5.2.4.4.1) ==========
+const addVisible = ref(false)
+const addFormRef = ref(null)
+const addForm = reactive({ deviceId: '', deviceCode: '', orderLevel: 2, faultTime: '', planFinishTime: '', orderDesc: '' })
+const addRules = {
+  orderLevel: [{ required: true, message: '请选择优先级', trigger: 'change' }],
+  orderDesc: [{ required: true, message: '请填写故障描述', trigger: 'blur' }]
+}
+const deviceOptions = ref([])
+const deviceLoading = ref(false)
+const deviceInfo = ref(null)
+const submitting = ref(false)
+
+function openAddDialog() {
+  addForm.deviceId = ''; addForm.deviceCode = ''; addForm.orderLevel = 2
+  addForm.faultTime = ''; addForm.planFinishTime = ''; addForm.orderDesc = ''
+  deviceOptions.value = []
+  deviceInfo.value = null
+  addVisible.value = true
+}
+
+async function searchDevice(query) {
+  if (!query) { deviceOptions.value = []; return }
+  deviceLoading.value = true
+  try {
+    const res = await getEquipmentFullPage(1, 20, { equipmentCode: query })
+    // 后端返回 MyBatis-Plus Page(records),兼容 rows 命名
+    if (res.data) deviceOptions.value = res.data.records || res.data.rows || []
+  } catch(e) { console.warn('设备搜索失败', e) }
+  finally { deviceLoading.value = false }
+}
+
+// 选中设备后:带出设备信息卡 + 自动填充设备编码
+function onDeviceChange(id) {
+  deviceInfo.value = null
+  if (!id) { addForm.deviceCode = ''; return }
+  // equipmentId 可能是数字/字符串,统一转字符串比较
+  const dev = deviceOptions.value.find(d => String(d.equipmentId) === String(id))
+  if (dev) {
+    deviceInfo.value = dev
+    addForm.deviceCode = dev.equipmentCode || ''
   }
+}
 
-  orders.value.unshift(newOrder)
-  console.log(`自动生成工单 ${newOrder.orderNo} 并派发给 ${maintainer}`)
+async function submitAdd() {
+  await addFormRef.value.validate().catch(() => { throw new Error('校验失败') })
+  submitting.value = true
+  try {
+    const data = { orderType: 1, orderLevel: addForm.orderLevel, orderDesc: addForm.orderDesc }
+    if (addForm.deviceId) data.deviceId = addForm.deviceId
+    if (addForm.deviceCode) data.deviceCode = addForm.deviceCode
+    if (addForm.planFinishTime) data.planFinishTime = addForm.planFinishTime
+    // 故障时间:手动指定则作为工单创建时间(列表"故障时间"列展示 createTime)
+    if (addForm.faultTime) data.createTime = addForm.faultTime
+    const res = await addWorkOrder(data)
+    ElMessage.success('工单创建成功')
+    addVisible.value = false
+    handleSearch()
+  } catch(e) { ElMessage.error('创建失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+  finally { submitting.value = false }
 }
 
-// ==================== 手动新增工单 ====================
-const showAddForm = ref(false)
-const newOrder = ref({
-  deviceType: '',
-  deviceId: '',
-  location: '',
-  manufacturer: '',
-  maintainer: '',
-  contact: '',
-  faultTime: '',
-  description: ''
-})
+// ========== 派单(部门树 + 用户多选,参考排水/燃气工单) ==========
+const dispatchDialogVisible = ref(false)
+const dispatchForm = reactive({ handlerIds: [], remarks: '' })
+const deptTreeRef = ref(null)
+const userTableRef = ref(null)
+const deptTreeData = ref([])
+const deptFilterText = ref('')
+const userList = ref([])
+const selectedUserList = ref([])
+
+watch(deptFilterText, (val) => { deptTreeRef.value && deptTreeRef.value.filter(val) })
+function filterDeptNode(value, data) {
+  if (!value) return true
+  return data.deptName.includes(value)
+}
 
-const openAddModal = () => {
-  showAddForm.value = true
-  newOrder.value = {
-    deviceType: '',
-    deviceId: '',
-    location: '',
-    manufacturer: '',
-    maintainer: '',
-    contact: '',
-    faultTime: new Date().toISOString().slice(0, 16),
-    description: ''
-  }
+async function loadDeptTree() {
+  if (deptTreeData.value.length > 0) return
+  try {
+    const res = await listDept()
+    deptTreeData.value = handleTree(res.data || [], 'deptId')
+  } catch(e) { console.warn('加载部门失败', e) }
+}
+
+async function handleDeptClick(node) {
+  try {
+    userTableRef.value && userTableRef.value.clearSelection()
+    selectedUserList.value = []
+    dispatchForm.handlerIds = []
+    const res = await listUser({ deptId: node.deptId, pageNum: 1, pageSize: 100 })
+    userList.value = res.rows || []
+  } catch(e) { console.warn('加载部门用户失败', e) }
 }
 
-const cancelAdd = () => {
-  showAddForm.value = false
+function handleSelectionChange(selection) {
+  selectedUserList.value = selection
+  dispatchForm.handlerIds = selection.map(item => item.userId)
 }
 
-const submitManualOrder = () => {
-  if (!newOrder.value.deviceType || !newOrder.value.deviceId || !newOrder.value.location || !newOrder.value.maintainer || !newOrder.value.faultTime || !newOrder.value.description) {
-    alert('请填写所有必填项')
+function handleRowClick(row) {
+  userTableRef.value && userTableRef.value.toggleRowSelection(row)
+}
+
+const selectedUserNames = computed(() => selectedUserList.value.map(item => item.nickName || item.userName))
+
+function handleDispatch() {
+  if (!canOperate.dispatch(selectedOrder.value)) {
+    ElMessage.warning('仅可对待派单工单进行派单操作')
     return
   }
+  Object.assign(dispatchForm, { handlerIds: [], remarks: '' })
+  selectedUserList.value = []
+  userList.value = []
+  loadDeptTree()
+  dispatchDialogVisible.value = true
+}
 
-  const contactMap = { '张工': '13800001111', '李工': '13800002222', '王工': '13800003333', '赵工': '13800004444' }
-  const faultTimeFormatted = newOrder.value.faultTime.replace('T', ' ')
-
-  const newOrderData = {
-    id: orders.value.length + 1,
-    orderNo: generateOrderNo(),
-    deviceType: newOrder.value.deviceType,
-    deviceId: newOrder.value.deviceId,
-    location: newOrder.value.location,
-    manufacturer: newOrder.value.manufacturer || '',
-    maintainer: newOrder.value.maintainer,
-    contact: newOrder.value.contact || contactMap[newOrder.value.maintainer] || '',
-    faultTime: faultTimeFormatted,
-    description: newOrder.value.description,
-    createTime: new Date().toLocaleString(),
-    status: 'pending',
-    startTime: null,
-    completeTime: null,
-    processResult: null
+async function submitDispatch() {
+  if (!dispatchForm.handlerIds || dispatchForm.handlerIds.length === 0) {
+    ElMessage.warning('请选择至少一个接单人')
+    return
   }
+  submitting.value = true
+  try {
+    await dispatchWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      handlerIds: dispatchForm.handlerIds,
+      remarks: dispatchForm.remarks
+    })
+    ElMessage.success('派单成功!已向 ' + dispatchForm.handlerIds.length + ' 人下发任务')
+    dispatchDialogVisible.value = false
+    refreshAfterAction()
+  } catch(e) { ElMessage.error('派单失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+  finally { submitting.value = false }
+}
+
+// ========== 核查/处理(参考排水/燃气工单) ==========
+const actionDialogVisible = ref(false)
+const actionTitle = ref('')
+const actionFormData = reactive({ solution: '' })
+const processBeforeImages = ref('')
+const imageUploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload'
 
-  orders.value.unshift(newOrderData)
-  alert(`工单 ${newOrderData.orderNo} 已创建并派发给 ${newOrder.value.maintainer}`)
-  showAddForm.value = false
+function handleVerify() {
+  if (!canOperate.verify(selectedOrder.value)) {
+    ElMessage.warning('当前工单不允许核查')
+    return
+  }
+  actionTitle.value = '核查工单'
+  actionFormData.solution = ''
+  processBeforeImages.value = ''
+  actionDialogVisible.value = true
 }
 
-// ==================== 工单操作 ====================
-const detailModalVisible = ref(false)
-const completeModalVisible = ref(false)
-const currentOrder = ref(null)
-const processResult = ref('')
+function handleProcess() {
+  if (!canOperate.process(selectedOrder.value)) {
+    ElMessage.warning('当前工单不允许处理')
+    return
+  }
 
-const viewOrderDetail = (order) => {
-  currentOrder.value = order
-  detailModalVisible.value = true
+  ElMessageBox.alert(
+    '<div style="padding: 10px; color: #666;">' +
+    '<strong>维修操作指引:</strong><br/><br/>' +
+    '• 在下方表格中可以上传处理前的照片(最多9张)<br/>' +
+    '• 请选择本地图片或拍照上传<br/>' +
+    '• 填写处理说明后点击"确定"提交<br/>' +
+    '</div>',
+    '开始维修',
+    {
+      dangerouslyUseHTMLString: true,
+      confirmButtonText: '我已了解',
+      center: false
+    }
+  ).then(() => {
+    actionTitle.value = '处理工单'
+    actionFormData.solution = ''
+    processBeforeImages.value = ''
+    actionDialogVisible.value = true
+  }).catch(() => {})
 }
 
-const closeDetailModal = () => {
-  detailModalVisible.value = false
-  currentOrder.value = null
+async function submitAction() {
+  if (!actionFormData.solution.trim()) {
+    ElMessage.warning('请填写说明')
+    return
+  }
+  submitting.value = true
+  try {
+    const data = {
+      orderId: selectedOrder.value.orderId,
+      handleResult: 'solved',
+      solution: actionFormData.solution
+    }
+    if (actionTitle.value === '处理工单' && processBeforeImages.value) {
+      data.maintenanceBeforeImg = processBeforeImages.value
+    }
+    if (actionTitle.value === '核查工单') {
+      await verifyWorkOrder(data)
+    } else {
+      await processWorkOrder(data)
+    }
+    ElMessage.success(actionTitle.value + '成功')
+    actionDialogVisible.value = false
+    refreshAfterAction()
+  } catch(e) { ElMessage.error('操作失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+  finally { submitting.value = false }
 }
 
-const updateStatus = (order, status) => {
-  order.status = status
-  if (status === 'processing') {
-    order.startTime = new Date().toLocaleString()
+// ========== 提交验收(后端 completeOrder 要求状态 4) ==========
+const completeDialogVisible = ref(false)
+const completeForm = reactive({ result: '' })
+const maintenanceAfterImages = ref('')
+
+function handleComplete() {
+  if (!canOperate.complete(selectedOrder.value)) {
+    ElMessage.warning('当前工单不允许提交验收')
+    return
   }
-  alert(`工单 ${order.orderNo} 状态已更新为${getStatusText(status)}`)
+  completeForm.result = ''
+  maintenanceAfterImages.value = ''
+  completeDialogVisible.value = true
 }
 
-const completeOrder = (order) => {
-  currentOrder.value = order
-  processResult.value = ''
-  completeModalVisible.value = true
+async function submitComplete() {
+  if (!completeForm.result.trim()) {
+    ElMessage.warning('请填写验收意见')
+    return
+  }
+  submitting.value = true
+  try {
+    await completeWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      result: completeForm.result,
+      maintenanceAfterImg: maintenanceAfterImages.value
+    })
+    ElMessage.success('提交验收成功')
+    completeDialogVisible.value = false
+    refreshAfterAction()
+  } catch(e) { ElMessage.error('提交失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+  finally { submitting.value = false }
 }
 
-const closeCompleteModal = () => {
-  completeModalVisible.value = false
-  currentOrder.value = null
-  processResult.value = ''
+// ========== 验收通过/结案(参考排水/燃气工单) ==========
+const closeDialogVisible = ref(false)
+const closeForm = reactive({ result: '' })
+
+function handleClose() {
+  if (!canOperate.close(selectedOrder.value)) {
+    ElMessage.warning('当前工单不允许验收通过')
+    return
+  }
+  closeForm.result = ''
+  closeDialogVisible.value = true
 }
 
-const submitComplete = () => {
-  if (currentOrder.value) {
-    currentOrder.value.status = 'completed'
-    currentOrder.value.completeTime = new Date().toLocaleString()
-    currentOrder.value.processResult = processResult.value || '处理完成'
-    alert(`工单 ${currentOrder.value.orderNo} 已完成`)
-    closeCompleteModal()
+async function submitClose() {
+  if (!closeForm.result.trim()) {
+    ElMessage.warning('请填写验收结果')
+    return
   }
+  submitting.value = true
+  try {
+    await closeWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      result: closeForm.result
+    })
+    ElMessage.success('验收通过,工单已结案')
+    closeDialogVisible.value = false
+    refreshAfterAction()
+  } catch(e) { ElMessage.error('验收失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+  finally { submitting.value = false }
 }
 
-// ==================== 查询相关 ====================
-const searchOrders = () => {
-  currentPage.value = 1
+// ========== 取消工单 ==========
+function handleCancel() {
+  ElMessageBox.confirm(`确定取消工单「${selectedOrder.value.orderNo}」吗?`, '取消工单', { type: 'warning' })
+    .then(async () => {
+      try {
+        await cancelWorkOrder(selectedOrder.value.orderId)
+        ElMessage.success('工单已取消')
+        selectedOrderId.value = null
+        selectedOrder.value = {}
+        handleSearch()
+      } catch(e) { ElMessage.error('取消失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+    }).catch(() => {})
 }
 
-const resetQuery = () => {
-  queryParams.value = { deviceType: '', deviceId: '', startDate: '', endDate: '' }
-  currentPage.value = 1
+/** 操作完成后刷新列表并重新加载当前选中工单 */
+async function refreshAfterAction() {
+  await handleSearch()
+  const fresh = tableData.value.find(o => o.orderId === selectedOrderId.value)
+  if (fresh) {
+    await handleSelectOrder(fresh)
+  } else {
+    selectedOrderId.value = null
+    selectedOrder.value = {}
+  }
 }
 
-// ==================== 生命周期 ====================
 onMounted(() => {
-  faultInterval = setInterval(simulateFaultReceive, 20000)
-})
-
-onUnmounted(() => {
-  if (faultInterval) clearInterval(faultInterval)
+  loadUsers()
+  handleSearch()
 })
 </script>
 
-<style scoped>
-* { margin: 0; padding: 0; box-sizing: border-box; }
-
-.prototype-container {
-  max-width: 1600px;
-  margin: 0 auto;
-  background: #f0f4f8;
-  border-radius: 28px;
-  box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.15);
-  overflow: hidden;
-  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
-}
+<style scoped lang="scss">
+.order-container {
+  padding: 0 16px 16px;
 
-/* 头部 */
-.header {
-  background: #0a2b3c;
-  padding: 16px 28px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 16px;
-  border-bottom: 3px solid #e67e22;
-}
+  .search-card {
+    margin-bottom: 16px;
+
+    :deep(.el-card__body) {
+      padding: 16px;
+    }
+  }
+
+  .search-form {
+    .el-form {
+      display: flex;
+      flex-wrap: wrap;
+      align-items: flex-start;
+      gap: 12px;
+
+      .el-form-item {
+        margin-bottom: 0;
+        min-width: 150px;
+
+        &:last-child {
+          margin-left: auto;
+        }
+      }
+    }
+  }
+
+  .main-row {
+    height: calc(100vh - 200px);
+
+    .col-fill {
+      height: 100%;
+    }
+  }
+
+  .box-card {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+
+    :deep(.el-card__body) {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      overflow: hidden;
+    }
+  }
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  .list-scroll {
+    flex: 1;
+    overflow: hidden;
+  }
+
+  .order-list {
+    padding-right: 4px;
+  }
+
+  .order-item {
+    padding: 16px;
+    margin-bottom: 8px;
+    border-radius: 8px;
+    cursor: pointer;
+    transition: all 0.2s;
+    border: 1px solid #e4e7ed;
+
+    &:hover {
+      background-color: #f5f7fa;
+    }
+
+    &.active {
+      background-color: #ecf5ff;
+      border-color: #409eff;
+    }
+
+    .order-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 8px;
 
-.logo h2 { color: white; font-weight: 600; font-size: 1.3rem; }
-.logo p { color: #bdd8e8; font-size: 0.7rem; }
-
-.header-stats { display: flex; gap: 12px; }
-.stat-card { background: rgba(255,255,255,0.15); padding: 6px 14px; border-radius: 40px; text-align: center; color: white; }
-.stat-card.pending { background: #f39c12; }
-.stat-card.processing { background: #3498db; }
-.stat-card.completed { background: #27ae60; }
-.stat-value { font-weight: 700; font-size: 1.2rem; margin-right: 6px; }
-.stat-label { font-size: 0.65rem; opacity: 0.9; }
-
-/* 布局 */
-.main-layout { display: flex; flex-wrap: wrap; }
-.left-panel { width: 380px; background: #f9fbfd; padding: 20px; }
-.right-panel { flex: 1; background: #ffffff; padding: 20px; min-width: 550px; }
-
-/* 卡片 */
-.card { background: white; border-radius: 20px; border: 1px solid #e6edf3; margin-bottom: 20px; overflow: hidden; }
-.card-header { background: #f9fafc; padding: 14px 18px; border-bottom: 1px solid #eef2f6; font-weight: 700; font-size: 0.85rem; color: #1d5a78; display: flex; justify-content: space-between; align-items: center; }
-.card-body { padding: 16px; }
-
-/* 实时故障监控 */
-.fault-monitor { max-height: 280px; overflow-y: auto; }
-.fault-item { background: #fef9e8; border-radius: 12px; padding: 10px; margin-bottom: 8px; border-left: 3px solid #e74c3c; }
-.fault-item.new { animation: highlight 1s ease; background: #fff3e0; }
-.fault-time { font-size: 0.6rem; color: #6f8ea8; font-family: monospace; }
-.fault-device { font-weight: 700; font-size: 0.75rem; margin: 4px 0; }
-.fault-message { font-size: 0.7rem; color: #e74c3c; }
-.fault-order-status { margin-top: 6px; font-size: 0.65rem; }
-.order-created { color: #27ae60; }
-.order-creating { color: #f39c12; }
-.empty-fault { text-align: center; padding: 30px; color: #8aaec2; }
-.live-badge { font-size: 0.65rem; background: #e74c3c; color: white; padding: 2px 10px; border-radius: 20px; animation: blink 1s infinite; }
-
-/* 手动新增表单 */
-.add-form { margin-top: 8px; }
-.empty-add { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 30px; color: #8aaec2; cursor: pointer; border: 1px dashed #cbdde6; border-radius: 16px; }
-.empty-add:hover { border-color: #e67e22; color: #e67e22; }
-.add-icon { font-size: 2rem; margin-bottom: 8px; }
-.form-row { display: flex; gap: 12px; margin-bottom: 12px; }
-.form-group { margin-bottom: 12px; }
-.form-group.half { flex: 1; }
-.form-label { font-size: 0.7rem; font-weight: 600; margin-bottom: 4px; display: block; color: #2c5a72; }
-.form-input, .form-select, .form-textarea { width: 100%; padding: 8px 12px; border-radius: 12px; border: 1px solid #cbdde6; font-size: 0.75rem; }
-.form-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 16px; }
-.btn-primary, .btn-outline, .btn-sm { padding: 5px 12px; border-radius: 30px; font-size: 0.7rem; cursor: pointer; }
-.btn-primary { background: #e67e22; border: none; color: white; }
-.btn-primary:hover { background: #d35400; }
-.btn-outline { background: white; border: 1px solid #e67e22; color: #e67e22; }
-
-/* 筛选栏 */
-.filter-bar { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; align-items: center; }
-.filter-select, .filter-input { padding: 6px 12px; border-radius: 20px; border: 1px solid #cbdde6; font-size: 0.7rem; }
-.filter-input { width: 130px; }
-.filter-sep { font-size: 0.7rem; color: #6f8ea8; }
-.list-count { font-size: 0.65rem; font-weight: normal; color: #6f8ea8; }
-
-/* 表格 */
-.order-table-wrapper { overflow-x: auto; }
-.data-table { width: 100%; border-collapse: collapse; font-size: 0.7rem; }
-.data-table th { background: #f8fafc; padding: 10px 8px; text-align: left; font-weight: 600; color: #2c5a72; border-bottom: 1px solid #e2edf2; }
-.data-table td { padding: 10px 8px; border-bottom: 1px solid #f0f4f7; vertical-align: middle; }
-.order-no { font-family: monospace; color: #e67e22; }
-.time-cell { white-space: nowrap; font-family: monospace; }
-.desc-cell { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.action-cell { white-space: nowrap; display: flex; gap: 6px; }
-.empty-row { text-align: center; padding: 30px; color: #8aaec2; }
-
-/* 状态标签 */
-.status-badge { padding: 2px 10px; border-radius: 20px; font-size: 0.65rem; }
-.status-pending { background: #fef3c7; color: #f39c12; }
-.status-processing { background: #e3f2fd; color: #3498db; }
-.status-completed { background: #d5f5e3; color: #27ae60; }
-
-/* 操作按钮 */
-.btn-view { background: #95a5a6; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-.btn-start { background: #3498db; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-.btn-complete { background: #27ae60; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-
-/* 分页 */
-.pagination { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
-.page-btn { background: white; border: 1px solid #cbdde6; padding: 4px 10px; border-radius: 20px; font-size: 0.7rem; cursor: pointer; }
-.page-btn.active { background: #e67e22; color: white; border-color: #e67e22; }
-
-/* 模态框 */
-.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
-.modal-content { background: white; border-radius: 24px; width: 480px; max-width: 90%; padding: 24px; max-height: 85vh; overflow-y: auto; }
-.detail-info { background: #f8fafc; border-radius: 16px; padding: 16px; margin-top: 8px; }
-.info-row { margin-bottom: 10px; font-size: 0.75rem; }
-.info-row .label { font-weight: 600; width: 100px; display: inline-block; color: #2c5a72; }
-.order-info-summary { background: #f8fafc; border-radius: 12px; padding: 12px; margin-bottom: 16px; font-size: 0.7rem; }
-
-@keyframes highlight { 0% { background: #fff3e0; } 100% { background: #fef9e8; } }
-@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } }
-</style>
+      .order-no {
+        font-weight: bold;
+        font-size: 14px;
+      }
+
+      .status-badge {
+        min-width: 60px;
+      }
+    }
+
+    .order-body {
+      font-size: 12px;
+      color: #606266;
+
+      .order-tags {
+        display: flex;
+        gap: 6px;
+        margin-bottom: 6px;
+      }
+
+      .order-desc {
+        margin-top: 4px;
+        color: #303133;
+        font-size: 13px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+
+      .device-code, .create-time {
+        margin-top: 4px;
+      }
+    }
+  }
+
+  .pagination-wrapper {
+    padding: 16px 0;
+    border-top: 1px solid #e4e7ed;
+    margin-top: 8px;
+
+    .pagination {
+      display: flex;
+      justify-content: flex-end;
+    }
+  }
+
+  .detail-body {
+    flex: 1;
+    overflow-y: auto;
+  }
+
+  .timeline-scroll {
+    max-height: calc(100vh - 340px);
+    overflow-y: auto;
+    padding-right: 8px;
+  }
+
+  .device-section, .image-section, .action-section {
+    margin-top: 24px;
+    padding-top: 24px;
+    border-top: 1px solid #e4e7ed;
+
+    .section-title {
+      font-size: 15px;
+      font-weight: bold;
+      margin-bottom: 16px;
+      color: #303133;
+    }
+  }
+
+  .image-list {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+  }
+
+  .image-group + .image-group {
+    margin-top: 16px;
+  }
+
+  .image-label {
+    display: block;
+    margin-bottom: 8px;
+    color: #606266;
+    font-size: 13px;
+  }
+
+  .action-buttons {
+    display: flex;
+    gap: 12px;
+    flex-wrap: wrap;
+
+    .el-button {
+      min-width: 100px;
+    }
+  }
+
+  /* 新增工单:设备信息卡(需求 5.2.4.4.1) */
+  .device-info-card {
+    width: 100%;
+    background: #f8fafc;
+    border-radius: 10px;
+    padding: 12px;
+    border: 1px solid #e4eaf1;
+
+    :deep(.el-descriptions__body) {
+      background: #fff;
+    }
+  }
+
+  .timeline-content {
+    .timestamp {
+      font-size: 12px;
+      color: #909399;
+      margin-bottom: 6px;
+    }
+
+    .operator-info {
+      font-size: 14px;
+      margin-bottom: 8px;
+
+      .operator-name {
+        font-weight: bold;
+        color: #303133;
+      }
+
+      .oper-type {
+        color: #909399;
+      }
+    }
+
+    .oper-desc {
+      font-size: 14px;
+      color: #606266;
+    }
+  }
+}
+</style>

+ 785 - 629
src/views/subSystem/pipeNetwork/pworkOrder/Pworkdisposition.vue

@@ -1,708 +1,864 @@
-<!--工单处置-->
+<!--全部工单处置(参考排水/燃气工单:搜索卡 + 左列表 + 右详情 tabs + 按钮操作 + 部门树派单)-->
+<!--数据源:/api/work-order/*(equipmentType 为空 = 查询所有设备工单)-->
 <template>
-  <div class="prototype-container">
-    <!-- 头部 -->
-    <div class="header">
-      <div class="logo">
-        <h2>🔧 故障工单处理系统</h2>
-        <p>工单处理 · 故障记录 · 附件上传 · 闭环管理</p>
+  <div class="order-container">
+    <!-- 顶部搜索区(参考排水/燃气工单) -->
+    <el-card class="search-card" shadow="never">
+      <div class="search-form">
+        <el-form :model="queryParams" ref="queryFormRef" inline>
+          <el-form-item label="类型">
+            <el-select v-model="queryParams.orderType" placeholder="全部类型" clearable style="width: 150px">
+              <el-option v-for="(v, k) in ORDER_TYPE_MAP" :key="k" :label="v.text" :value="Number(k)"/>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="优先级">
+            <el-select v-model="queryParams.orderLevel" placeholder="全部优先级" clearable style="width: 120px">
+              <el-option v-for="(v, k) in ORDER_LEVEL_MAP" :key="k" :label="v.text" :value="Number(k)"/>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="状态">
+            <el-select v-model="queryParams.orderStatus" placeholder="全部状态" clearable style="width: 120px">
+              <el-option v-for="(v, k) in ORDER_STATUS_MAP" :key="k" :label="v.text" :value="Number(k)"/>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="关键词">
+            <el-input v-model="queryParams.keyword" placeholder="工单编号/设备编码" clearable style="width: 200px" @keyup.enter="handleSearch"/>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="handleSearch">查询</el-button>
+            <el-button @click="handleReset">重置</el-button>
+          </el-form-item>
+        </el-form>
       </div>
-      <div class="header-stats">
-        <div class="stat-card pending">
-          <span class="stat-value">{{ pendingCount }}</span>
-          <span class="stat-label">待处理</span>
-        </div>
-        <div class="stat-card processing">
-          <span class="stat-value">{{ processingCount }}</span>
-          <span class="stat-label">处理中</span>
-        </div>
-        <div class="stat-card completed">
-          <span class="stat-value">{{ completedCount }}</span>
-          <span class="stat-label">已完成</span>
-        </div>
-      </div>
-    </div>
+    </el-card>
 
-    <div class="main-layout">
+    <!-- 主容器:左列表 + 右详情(参考排水/燃气工单) -->
+    <el-row :gutter="16" class="main-row">
       <!-- 左侧:工单列表 -->
-      <div class="left-panel">
-        <div class="card">
-          <div class="card-header">
-            📋 我的工单列表
-            <div class="filter-bar">
-              <select v-model="filterStatus" class="filter-select">
-                <option value="">全部状态</option>
-                <option value="pending">待处理</option>
-                <option value="processing">处理中</option>
-                <option value="completed">已完成</option>
-              </select>
-              <input type="text" v-model="searchKeyword" class="search-input" placeholder="搜索工单号/设备">
-              <button @click="searchOrders" class="btn-primary btn-sm">查询</button>
+      <el-col :span="8" class="col-fill">
+        <el-card class="box-card" shadow="always">
+          <template #header>
+            <div class="card-header">
+              <span>工单列表</span>
             </div>
-          </div>
-          <div class="card-body">
+          </template>
+          <el-scrollbar class="list-scroll">
             <div class="order-list">
-              <div v-for="order in paginatedOrders" :key="order.id" class="order-item" :class="order.status">
+              <div
+                v-for="order in tableData"
+                :key="order.orderId"
+                class="order-item"
+                :class="{ 'active': selectedOrderId === order.orderId }"
+                @click="handleSelectOrder(order)"
+              >
                 <div class="order-header">
-                  <div class="order-id">
-                    <span class="order-no">{{ order.orderNo }}</span>
-                    <span class="order-type" :class="order.priority === 'urgent' ? 'type-urgent' : 'type-normal'">
-                      {{ order.priority === 'urgent' ? '紧急' : '普通' }}
-                    </span>
-                  </div>
-                  <div class="order-status" :class="getStatusClass(order.status)">
-                    {{ getStatusText(order.status) }}
-                  </div>
-                </div>
-                <div class="order-device">
-                  <span class="label">设备:</span>
-                  <span class="value">{{ order.deviceName }}</span>
-                  <span class="label">位置:</span>
-                  <span class="value">{{ order.location }}</span>
-                </div>
-                <div class="order-desc">
-                  <span class="label">故障描述:</span>
-                  <span class="value">{{ order.description }}</span>
-                </div>
-                <div class="order-time">
-                  <span class="label">故障时间:</span>
-                  <span class="value">{{ order.faultTime }}</span>
-                </div>
-                <div class="order-actions">
-                  <button v-if="order.status === 'pending'" @click="startProcess(order)" class="btn-start">开始处理</button>
-                  <button v-if="order.status === 'processing'" @click="openProcessModal(order)" class="btn-process">处理反馈</button>
-                  <button @click="viewOrderDetail(order)" class="btn-view">查看详情</button>
-                </div>
-              </div>
-              <div v-if="filteredOrders.length === 0" class="empty-order">
-                暂无工单数据
-              </div>
-            </div>
-            <div class="pagination" v-if="totalPages > 1">
-              <button v-for="page in totalPages" :key="page" class="page-btn" :class="{ active: currentPage === page }"
-                      @click="currentPage = page">{{ page }}</button>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 右侧:工单处理面板 -->
-      <div class="right-panel">
-        <div class="card" v-if="currentProcessingOrder">
-          <div class="card-header">
-            🔧 工单处理
-            <span class="order-badge">{{ currentProcessingOrder.orderNo }}</span>
-          </div>
-          <div class="card-body">
-            <div class="process-info">
-              <div class="info-row"><span class="label">设备名称:</span>{{ currentProcessingOrder.deviceName }}</div>
-              <div class="info-row"><span class="label">设备位置:</span>{{ currentProcessingOrder.location }}</div>
-              <div class="info-row"><span class="label">故障描述:</span>{{ currentProcessingOrder.description }}</div>
-              <div class="info-row"><span class="label">故障时间:</span>{{ currentProcessingOrder.faultTime }}</div>
-            </div>
-
-            <div class="process-form">
-              <div class="form-title">处理反馈信息</div>
-              <div class="form-row">
-                <div class="form-group half">
-                  <label class="form-label">故障类型 *</label>
-                  <select v-model="processData.faultType" class="form-select">
-                    <option value="">请选择</option>
-                    <option value="硬件故障">硬件故障</option>
-                    <option value="软件故障">软件故障</option>
-                    <option value="通信故障">通信故障</option>
-                    <option value="电源故障">电源故障</option>
-                    <option value="传感器故障">传感器故障</option>
-                    <option value="线路故障">线路故障</option>
-                    <option value="其他">其他</option>
-                  </select>
-                </div>
-                <div class="form-group half">
-                  <label class="form-label">处理时间 *</label>
-                  <input type="datetime-local" v-model="processData.processTime" class="form-input">
-                </div>
-              </div>
-              <div class="form-group">
-                <label class="form-label">故障原因 *</label>
-                <textarea v-model="processData.faultReason" class="form-textarea" rows="2" placeholder="请详细描述故障原因..."></textarea>
-              </div>
-              <div class="form-row">
-                <div class="form-group half">
-                  <label class="form-label">处理人员 *</label>
-                  <input v-model="processData.handler" class="form-input" placeholder="处理人员姓名">
-                </div>
-                <div class="form-group half">
-                  <label class="form-label">联系方式 *</label>
-                  <input v-model="processData.contact" class="form-input" placeholder="联系电话">
+                  <span class="order-no">{{ order.orderNo }}</span>
+                  <el-tag :type="orderStatusType(order.orderStatus)" size="small" effect="light" class="status-badge">{{ orderStatusText(order.orderStatus) }}</el-tag>
                 </div>
-              </div>
-              <div class="form-group">
-                <label class="form-label">处理措施</label>
-                <textarea v-model="processData.processMeasure" class="form-textarea" rows="2" placeholder="请描述处理措施..."></textarea>
-              </div>
-              <div class="form-group">
-                <label class="form-label">附件上传</label>
-                <div class="upload-area">
-                  <label class="upload-btn">
-                    📎 选择图片/文件
-                    <input type="file" @change="handleFileUpload" accept="image/*,.pdf,.doc" hidden multiple>
-                  </label>
-                  <div class="file-list">
-                    <div v-for="(file, idx) in uploadedFiles" :key="idx" class="file-item">
-                      <img v-if="file.type?.startsWith('image/')" :src="file.preview" class="file-preview">
-                      <span class="file-name">{{ file.name }}</span>
-                      <button @click="removeFile(idx)" class="remove-file">✖</button>
-                    </div>
+                <div class="order-body">
+                  <div class="order-tags">
+                    <el-tag :type="orderLevelType(order.orderLevel)" size="small" effect="plain">{{ orderLevelText(order.orderLevel) }}</el-tag>
+                    <el-tag :type="orderTypeTagType(order.orderType)" size="small" effect="plain">{{ orderTypeText(order.orderType) }}</el-tag>
                   </div>
-                  <div class="upload-hint">支持上传图片、PDF、Word文档,最多5个文件</div>
+                  <div class="order-desc" v-if="order.orderDesc">{{ order.orderDesc }}</div>
+                  <div class="device-code">设备编号:{{ order.deviceCode || '-' }}</div>
+                  <div class="device-code" v-if="order.hazardNo">关联隐患:{{ order.hazardNo }}</div>
+                  <div class="create-time">创建:{{ timeText(order.createTime) }}</div>
                 </div>
               </div>
-              <div class="form-actions">
-                <button @click="cancelProcess" class="btn-outline">取消</button>
-                <button @click="submitProcess" class="btn-primary">提交处理</button>
-              </div>
+              <el-empty v-if="tableData.length === 0" description="暂无数据" :image-size="100"/>
             </div>
+          </el-scrollbar>
+          <div class="pagination-wrapper">
+            <el-pagination
+              v-model:current-page="pageNum"
+              v-model:page-size="pageSize"
+              :total="total"
+              layout="total, prev, pager, next"
+              :page-sizes="[10, 20, 50, 100]"
+              @size-change="handleSizeChange"
+              @current-change="handlePageChange"
+              class="pagination"
+            />
           </div>
-        </div>
-
-        <!-- 未选择工单时的提示 -->
-        <div class="card" v-else>
-          <div class="card-header">
-            🔧 工单处理
-          </div>
-          <div class="card-body">
-            <div class="empty-process">
-              <div class="empty-icon">🔧</div>
-              <div>请从左侧选择待处理的工单</div>
-              <div class="empty-hint">点击「开始处理」或「处理反馈」按钮</div>
+        </el-card>
+      </el-col>
+
+      <!-- 右侧:工单详情 -->
+      <el-col :span="16" class="col-fill">
+        <el-card v-loading="detailLoading" class="box-card" shadow="always">
+          <template #header>
+            <div class="card-header">
+              <span v-if="selectedOrder && selectedOrder.orderId">工单详情 - {{ selectedOrder.orderNo }}</span>
+              <span v-else>请选择工单查看详情</span>
             </div>
-          </div>
-        </div>
+          </template>
+
+          <div v-if="selectedOrder && selectedOrder.orderId" class="detail-body">
+            <!-- 标签页(参考排水/燃气工单) -->
+            <el-tabs v-model="activeTab" class="detail-tabs">
+              <!-- 工单信息 -->
+              <el-tab-pane label="工单信息" name="info">
+                <el-descriptions :column="2" border size="small">
+                  <el-descriptions-item label="工单编号">{{ selectedOrder.orderNo }}</el-descriptions-item>
+                  <el-descriptions-item label="工单状态">
+                    <el-tag :type="orderStatusType(selectedOrder.orderStatus)" size="small">{{ orderStatusText(selectedOrder.orderStatus) }}</el-tag>
+                  </el-descriptions-item>
+                  <el-descriptions-item label="关联案件编号">{{ selectedOrder.caseNo || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="关联隐患编号">{{ selectedOrder.hazardNo || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="设备编号">{{ selectedOrder.deviceCode || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="工单类型">{{ orderTypeText(selectedOrder.orderType) }}</el-descriptions-item>
+                  <el-descriptions-item label="优先级">
+                    <el-tag :type="orderLevelType(selectedOrder.orderLevel)" size="small">{{ orderLevelText(selectedOrder.orderLevel) }}</el-tag>
+                  </el-descriptions-item>
+                  <el-descriptions-item label="接单人">{{ userNames(selectedOrder.receiveUser) || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="问题描述" :span="2">{{ selectedOrder.orderDesc || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="派单时间">{{ timeText(selectedOrder.dispatchTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="计划完成">{{ timeText(selectedOrder.planFinishTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="实际完成">{{ timeText(selectedOrder.realFinishTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="验收时间">{{ timeText(selectedOrder.verifyTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="创建时间">{{ timeText(selectedOrder.createTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="更新时间">{{ timeText(selectedOrder.updateTime) }}</el-descriptions-item>
+                  <el-descriptions-item label="处置结果" :span="2">{{ selectedOrder.repairResult || '-' }}</el-descriptions-item>
+                </el-descriptions>
+
+                <!-- 关联设备信息(需求 5.2.4.4.1 设备维度:类型/位置/厂家/维护人员/联系方式) -->
+                <div v-if="detailDevice" class="device-section">
+                  <h4 class="section-title">🔗 关联设备</h4>
+                  <el-descriptions :column="2" border size="small">
+                    <el-descriptions-item label="设备类型">{{ detailDevice.equipmentTypeName || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="设备编码">{{ detailDevice.equipmentCode || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="安装位置">{{ detailDevice.equipmentLocation || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="管点地址">{{ detailDevice.pointAddress || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="设备厂家">{{ detailDevice.manufacturer || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="所属管线">{{ detailDevice.networkName || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="维护人员">{{ detailDevice.maintainer || '-' }}</el-descriptions-item>
+                    <el-descriptions-item label="联系方式">{{ detailDevice.maintainerPhone || '-' }}</el-descriptions-item>
+                  </el-descriptions>
+                </div>
 
-        <!-- 已完成工单详情展示 -->
-        <div class="card" v-if="selectedCompletedOrder">
-          <div class="card-header">
-            ✅ 已完成工单详情
-            <span class="order-badge">{{ selectedCompletedOrder.orderNo }}</span>
-          </div>
-          <div class="card-body">
-            <div class="completed-info">
-              <div class="info-group">
-                <div class="group-title">基本信息</div>
-                <div class="info-row"><span class="label">设备名称:</span>{{ selectedCompletedOrder.deviceName }}</div>
-                <div class="info-row"><span class="label">设备位置:</span>{{ selectedCompletedOrder.location }}</div>
-                <div class="info-row"><span class="label">故障描述:</span>{{ selectedCompletedOrder.description }}</div>
-              </div>
-              <div class="info-group">
-                <div class="group-title">处理信息</div>
-                <div class="info-row"><span class="label">故障类型:</span>{{ selectedCompletedOrder.faultType || '-' }}</div>
-                <div class="info-row"><span class="label">故障原因:</span>{{ selectedCompletedOrder.faultReason || '-' }}</div>
-                <div class="info-row"><span class="label">处理措施:</span>{{ selectedCompletedOrder.processMeasure || '-' }}</div>
-                <div class="info-row"><span class="label">处理人员:</span>{{ selectedCompletedOrder.handler }}</div>
-                <div class="info-row"><span class="label">联系方式:</span>{{ selectedCompletedOrder.contact }}</div>
-                <div class="info-row"><span class="label">处理时间:</span>{{ selectedCompletedOrder.processTime }}</div>
-                <div class="info-row"><span class="label">完成时间:</span>{{ selectedCompletedOrder.completeTime }}</div>
-              </div>
-              <div class="info-group" v-if="selectedCompletedOrder.attachments?.length">
-                <div class="group-title">附件资料</div>
-                <div class="attachment-list">
-                  <div v-for="(att, idx) in selectedCompletedOrder.attachments" :key="idx" class="attachment-item">
-                    <img v-if="att.type?.startsWith('image/')" :src="att.preview" class="attachment-img">
-                    <span class="attachment-name">{{ att.name }}</span>
+                <RepairImagePanel :before-images="selectedOrder.maintenanceBeforeImg" :after-images="selectedOrder.maintenanceAfterImg" />
+
+                <!-- 工单操作(参考排水/燃气工单按钮组) -->
+                <div class="action-section">
+                  <h4 class="section-title">工单操作</h4>
+                  <div class="action-buttons">
+                    <el-button v-if="canOperate.dispatch(selectedOrder)" type="primary" @click="handleDispatch">下派任务</el-button>
+                    <el-button v-if="canOperate.verify(selectedOrder)" type="warning" @click="handleVerify">核查</el-button>
+                    <el-button v-if="canOperate.process(selectedOrder)" type="success" @click="handleProcess">处理</el-button>
+                    <el-button v-if="canOperate.complete(selectedOrder)" type="danger" @click="handleComplete">提交验收</el-button>
+                    <el-button v-if="canOperate.close(selectedOrder)" type="success" @click="handleClose">验收通过</el-button>
+                    <el-button v-if="canOperate.cancel(selectedOrder)" type="info" plain @click="handleCancel">取消工单</el-button>
                   </div>
                 </div>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- 处理反馈模态框(备用) -->
-    <div v-if="processModalVisible" class="modal-overlay" @click.self="closeProcessModal">
-      <div class="modal-content" style="width:600px;">
-        <h3>🔧 工单处理反馈</h3>
-        <div class="order-summary">
-          <div><span class="label">工单号:</span>{{ currentOrder?.orderNo }}</div>
-          <div><span class="label">设备:</span>{{ currentOrder?.deviceName }}</div>
-        </div>
-        <div class="process-form-modal">
-          <div class="form-row">
-            <div class="form-group half">
-              <label class="form-label">故障类型 *</label>
-              <select v-model="modalProcessData.faultType" class="form-select">
-                <option value="">请选择</option>
-                <option value="硬件故障">硬件故障</option>
-                <option value="软件故障">软件故障</option>
-                <option value="通信故障">通信故障</option>
-                <option value="电源故障">电源故障</option>
-                <option value="传感器故障">传感器故障</option>
-                <option value="线路故障">线路故障</option>
-              </select>
-            </div>
-            <div class="form-group half">
-              <label class="form-label">处理时间 *</label>
-              <input type="datetime-local" v-model="modalProcessData.processTime" class="form-input">
-            </div>
-          </div>
-          <div class="form-group">
-            <label class="form-label">故障原因 *</label>
-            <textarea v-model="modalProcessData.faultReason" class="form-textarea" rows="2" placeholder="请详细描述故障原因..."></textarea>
-          </div>
-          <div class="form-row">
-            <div class="form-group half">
-              <label class="form-label">处理人员 *</label>
-              <input v-model="modalProcessData.handler" class="form-input">
-            </div>
-            <div class="form-group half">
-              <label class="form-label">联系方式 *</label>
-              <input v-model="modalProcessData.contact" class="form-input">
-            </div>
-          </div>
-          <div class="form-group">
-            <label class="form-label">处理措施</label>
-            <textarea v-model="modalProcessData.processMeasure" class="form-textarea" rows="2"></textarea>
-          </div>
-          <div class="form-group">
-            <label class="form-label">附件上传</label>
-            <div class="upload-area">
-              <label class="upload-btn">
-                📎 上传文件
-                <input type="file" @change="handleModalFileUpload" accept="image/*,.pdf" hidden multiple>
-              </label>
-              <div class="file-list">
-                <span v-for="(file, idx) in modalUploadedFiles" :key="idx" class="file-tag">
-                  {{ file.name }}
-                  <button @click="removeModalFile(idx)" class="remove-file">✖</button>
-                </span>
-              </div>
-            </div>
+              </el-tab-pane>
+
+              <!-- 处理历史 -->
+              <el-tab-pane label="处理历史" name="history">
+                <div class="timeline-scroll">
+                  <el-timeline v-if="currentOrderLogs && currentOrderLogs.length > 0">
+                    <el-timeline-item
+                      v-for="log in currentOrderLogs"
+                      :key="log.logId || log.id"
+                      :timestamp="timeText(log.operTime)"
+                      placement="top"
+                      :type="orderLogType(log.operType)"
+                      size="large"
+                    >
+                      <el-card shadow="hover">
+                        <div class="timeline-content">
+                          <div class="timestamp">{{ timeText(log.operTime) }}</div>
+                          <div class="operator-info">
+                            <span class="operator-name">{{ log.operUserName || '-' }}</span>
+                            <span class="oper-type">· {{ orderLogText(log.operType) }}</span>
+                          </div>
+                          <div class="oper-desc">{{ log.operDesc || '-' }}</div>
+                        </div>
+                      </el-card>
+                    </el-timeline-item>
+                  </el-timeline>
+                  <el-empty v-else description="暂无处理记录" :image-size="100"/>
+                </div>
+              </el-tab-pane>
+            </el-tabs>
           </div>
+
+          <el-empty v-else description="请从左侧选择工单" :image-size="150"/>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 派单弹窗(部门树 + 用户多选,参考排水/燃气工单) -->
+    <el-dialog v-model="dispatchDialogVisible" title="下派任务 - 选择接单人" width="750px" append-to-body>
+      <div style="display: flex; gap: 16px; min-height: 350px;">
+        <!-- 左侧:部门树 -->
+        <div style="width: 280px; border-right: 1px solid #e4e7ed; padding-right: 12px;">
+          <el-input v-model="deptFilterText" placeholder="筛选部门" clearable size="small" style="margin-bottom: 8px"/>
+          <el-tree
+            ref="deptTreeRef"
+            :data="deptTreeData"
+            :props="{ label: 'deptName', children: 'children' }"
+            :filter-node-method="filterDeptNode"
+            node-key="deptId"
+            highlight-current
+            @node-click="handleDeptClick"
+            style="font-size: 14px"
+          />
         </div>
-        <div class="form-actions">
-          <button @click="closeProcessModal" class="btn-outline">取消</button>
-          <button @click="submitModalProcess" class="btn-primary">提交处理</button>
+        <!-- 右侧:用户列表 -->
+        <div style="flex: 1; min-width: 0;">
+          <el-table
+            ref="userTableRef"
+            :data="userList"
+            border size="small"
+            max-height="300"
+            row-key="userId"
+            @selection-change="handleSelectionChange"
+            @row-click="handleRowClick"
+          >
+            <el-table-column type="selection" width="40" align="center" :reserve-selection="true"/>
+            <el-table-column prop="userName" label="用户名" width="100"/>
+            <el-table-column prop="nickName" label="昵称" width="100"/>
+            <el-table-column prop="dept.deptName" label="部门" show-overflow-tooltip/>
+          </el-table>
+          <div v-if="userList.length === 0" style="text-align: center; color: #909399; padding: 40px 0;">请先在左侧选择部门</div>
         </div>
       </div>
-    </div>
+      <el-form :model="dispatchForm" label-width="80px" style="margin-top: 16px">
+        <el-form-item label="备注"><el-input v-model="dispatchForm.remarks" placeholder="请输入派单备注"/></el-form-item>
+      </el-form>
+      <template #footer>
+        <span style="float: left; line-height: 32px; color: #606266;">
+          已选人员:
+          <span v-if="selectedUserNames.length > 0" style="color: #409eff; font-weight: bold;">{{ selectedUserNames.join('、') }}</span>
+          <span v-else style="color: #909399">未选择</span>
+          ({{ dispatchForm.handlerIds.length }})
+        </span>
+        <el-button @click="dispatchDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitDispatch">确认</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 核查/处理弹窗(参考排水/燃气工单) -->
+    <el-dialog v-model="actionDialogVisible" :title="actionTitle" width="600px" append-to-body>
+      <el-form label-width="90px">
+        <el-form-item v-if="actionTitle === '处理工单'" label="处理前照片">
+          <ImageUpload v-model="processBeforeImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
+        </el-form-item>
+        <el-form-item label="结果说明" required>
+          <el-input v-model="actionFormData.solution" type="textarea" :rows="5" :placeholder="actionTitle === '核查工单' ? '请输入核查结论(属实/不属实、现场情况)' : '请输入故障原因、处理结果及处理措施'" maxlength="500" show-word-limit/>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="actionDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitAction">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 提交验收弹窗 -->
+    <el-dialog v-model="completeDialogVisible" title="提交验收" width="600px" append-to-body>
+      <el-form label-width="80px">
+        <el-form-item label="维修后照片">
+          <ImageUpload v-model="maintenanceAfterImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
+        </el-form-item>
+        <el-form-item label="验收意见">
+          <el-input v-model="completeForm.result" type="textarea" :rows="4" placeholder="请填写维修处置结果说明" maxlength="500" show-word-limit/>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="completeDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitComplete">提交</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 验收通过/结案弹窗 -->
+    <el-dialog v-model="closeDialogVisible" title="验收通过" width="500px" append-to-body>
+      <el-form label-width="80px">
+        <el-form-item label="验收结果">
+          <el-input v-model="closeForm.result" type="textarea" :rows="4" placeholder="请填写验收结果(结案后关联隐患同步为已整改、设备报警状态重置)" maxlength="500" show-word-limit/>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="closeDialogVisible = false">取消</el-button>
+        <el-button type="success" :loading="submitting" @click="submitClose">确认验收</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue'
-
-// ==================== 工单数据 ====================
-const orders = ref([
-  {
-    id: 1, orderNo: 'WO-20250508-001', priority: 'urgent', deviceName: '压力传感器-PS001', location: '城东泵站',
-    description: '设备离线,数据上报异常,超过30分钟未回传', faultTime: '2025-05-08 09:23:15',
-    status: 'pending', assignedTo: '张工'
-  },
-  {
-    id: 2, orderNo: 'WO-20250508-002', priority: 'normal', deviceName: '泄漏检测仪-LD005', location: '化工园区',
-    description: '可燃气浓度超阈值报警,需现场核查', faultTime: '2025-05-08 07:30:22',
-    status: 'processing', assignedTo: '李工',
-    faultType: '传感器故障', faultReason: '传感器灵敏度漂移', processMeasure: '已重新校准传感器',
-    handler: '李工', contact: '13800002222', processTime: '2025-05-08 10:30:00'
-  },
-  {
-    id: 3, orderNo: 'WO-20250507-003', priority: 'normal', deviceName: '水位监测仪-WL006', location: '东区排水口',
-    description: '水位数据异常波动,需校准', faultTime: '2025-05-07 14:20:33',
-    status: 'completed', assignedTo: '王工',
-    faultType: '硬件故障', faultReason: '传感器进水导致读数异常', processMeasure: '更换传感器,已恢复正常',
-    handler: '王工', contact: '13800003333', processTime: '2025-05-07 16:30:00', completeTime: '2025-05-07 17:00:00',
-    attachments: [{ name: '维修现场.jpg', type: 'image/jpeg', preview: null }]
-  },
-  {
-    id: 4, orderNo: 'WO-20250508-004', priority: 'normal', deviceName: '流量传感器-FL002', location: '城南枢纽',
-    description: '流量数据中断,设备可能离线', faultTime: '2025-05-08 06:15:00',
-    status: 'pending', assignedTo: '张工'
-  }
-])
-
-// 当前正在处理的工单
-const currentProcessingOrder = ref(null)
-const selectedCompletedOrder = ref(null)
-
-// 处理表单数据
-const processData = ref({
-  faultType: '',
-  processTime: '',
-  faultReason: '',
-  handler: '',
-  contact: '',
-  processMeasure: ''
-})
-const uploadedFiles = ref([])
-
-// 模态框相关
-const processModalVisible = ref(false)
-const currentOrder = ref(null)
-const modalProcessData = ref({
-  faultType: '',
-  processTime: '',
-  faultReason: '',
-  handler: '',
-  contact: '',
-  processMeasure: ''
-})
-const modalUploadedFiles = ref([])
-
-// 筛选参数
-const filterStatus = ref('')
-const searchKeyword = ref('')
-const currentPage = ref(1)
-const pageSize = 5
-
-// 过滤后的工单
-const filteredOrders = computed(() => {
-  let result = [...orders.value]
-  if (filterStatus.value) {
-    result = result.filter(o => o.status === filterStatus.value)
-  }
-  if (searchKeyword.value) {
-    const kw = searchKeyword.value.toLowerCase()
-    result = result.filter(o => o.orderNo.toLowerCase().includes(kw) || o.deviceName.toLowerCase().includes(kw))
-  }
-  // 按故障时间倒序
-  return result.sort((a, b) => new Date(b.faultTime) - new Date(a.faultTime))
-})
+import { ref, reactive, computed, watch, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import RepairImagePanel from '@/components/RepairImagePanel'
+import ImagePreview from '@/components/ImagePreview'
+import { listWorkOrder, getWorkOrderDetail, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder, cancelWorkOrder } from '@/api/work/order'
+import { getEquipmentFullPage } from '@/api/pipeNetwork/basic'
+import { listDept } from '@/api/system/dept'
+import { listUser } from '@/api/system/user'
+import { handleTree } from '@/utils/ruoyi'
+import { ORDER_STATUS_MAP, ORDER_TYPE_MAP, ORDER_LEVEL_MAP, orderStatusText, orderStatusType, orderTypeText, orderTypeTagType, orderLevelText, orderLevelType, orderLogText, orderLogType, canOperate, timeText } from './workOrderDict'
+
+// ========== 查询 & 列表(equipmentType 为空 = 查询所有设备工单) ==========
+const listLoading = ref(false)
+const tableData = ref([])
+const pageNum = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const queryFormRef = ref(null)
+const queryParams = reactive({ orderType: null, orderLevel: null, orderStatus: null, keyword: '' })
+
+async function handleSearch() {
+  listLoading.value = true
+  try {
+    const params = {}
+    if (queryParams.orderType) params.orderType = queryParams.orderType
+    if (queryParams.orderLevel) params.orderLevel = queryParams.orderLevel
+    if (queryParams.orderStatus) params.orderStatus = queryParams.orderStatus
+    if (queryParams.keyword) params.keyword = queryParams.keyword
+    const res = await listWorkOrder(params, pageNum.value, pageSize.value)
+    if (res.data) {
+      tableData.value = res.data.records || []
+      total.value = Number(res.data.total || 0)
+    }
+  } catch(e) { ElMessage.error('查询失败'); console.error(e) }
+  finally { listLoading.value = false }
+}
 
-const paginatedOrders = computed(() => {
-  const start = (currentPage.value - 1) * pageSize
-  return filteredOrders.value.slice(start, start + pageSize)
-})
+function handleReset() {
+  queryFormRef.value && queryFormRef.value.resetFields()
+  queryParams.orderType = null
+  queryParams.orderLevel = null
+  queryParams.orderStatus = null
+  queryParams.keyword = ''
+  pageNum.value = 1
+  selectedOrderId.value = null
+  selectedOrder.value = {}
+  handleSearch()
+}
 
-const totalPages = computed(() => Math.ceil(filteredOrders.value.length / pageSize))
+// ========== 选中工单 & 详情(合并 res.data.order + logs,参考排水/燃气工单) ==========
+const selectedOrderId = ref(null)
+const selectedOrder = ref({})
+const activeTab = ref('info')
+const currentOrderLogs = ref([])
+const detailLoading = ref(false)
+const detailDevice = ref(null)
+
+// 工单图片列表(多个用逗号分隔)
+async function handleSelectOrder(order) {
+  selectedOrderId.value = order.orderId
+  selectedOrder.value = { ...order }
+  activeTab.value = 'info'
+  currentOrderLogs.value = []
+  detailDevice.value = null
+  await loadOrderDetail(order.orderId)
+}
 
-// 统计数据
-const pendingCount = computed(() => orders.value.filter(o => o.status === 'pending').length)
-const processingCount = computed(() => orders.value.filter(o => o.status === 'processing').length)
-const completedCount = computed(() => orders.value.filter(o => o.status === 'completed').length)
+async function loadOrderDetail(orderId) {
+  detailLoading.value = true
+  try {
+    const res = await getWorkOrderDetail(orderId)
+    if (res.code === 200 && res.data) {
+      selectedOrder.value = { ...selectedOrder.value, ...res.data.order }
+      currentOrderLogs.value = res.data.logs || []
+      // 关联设备全信息(需求字段:类型/位置/厂家/维护人员/联系方式)
+      if (res.data.order?.deviceCode) {
+        loadDeviceInfo(res.data.order.deviceCode)
+      }
+    }
+  } catch(e) { ElMessage.error('加载工单详情失败'); console.error(e) }
+  finally { detailLoading.value = false }
+}
 
-// ==================== 辅助函数 ====================
-const getStatusText = (status) => {
-  const map = { pending: '待处理', processing: '处理中', completed: '已完成' }
-  return map[status] || status
+async function loadDeviceInfo(codeOrId) {
+  try {
+    const res = await getEquipmentFullPage(1, 1, { equipmentCode: codeOrId })
+    const rows = res.data?.records || res.data?.rows || []
+    if (rows.length) detailDevice.value = rows[0]
+  } catch(e) { console.warn('加载设备信息失败', e) }
 }
 
-const getStatusClass = (status) => {
-  const map = { pending: 'status-pending', processing: 'status-processing', completed: 'status-completed' }
-  return map[status] || ''
+function handleSizeChange() {
+  pageNum.value = 1
+  selectedOrderId.value = null
+  selectedOrder.value = {}
+  handleSearch()
 }
 
-// ==================== 工单操作 ====================
-const startProcess = (order) => {
-  order.status = 'processing'
-  currentProcessingOrder.value = order
-  // 预填处理人员信息
-  processData.value = {
-    faultType: '',
-    processTime: new Date().toISOString().slice(0, 16),
-    faultReason: '',
-    handler: order.assignedTo || '',
-    contact: '',
-    processMeasure: ''
-  }
-  uploadedFiles.value = []
-  selectedCompletedOrder.value = null
+function handlePageChange() {
+  selectedOrderId.value = null
+  selectedOrder.value = {}
+  handleSearch()
 }
 
-const openProcessModal = (order) => {
-  currentOrder.value = order
-  currentProcessingOrder.value = order
-  modalProcessData.value = {
-    faultType: order.faultType || '',
-    processTime: new Date().toISOString().slice(0, 16),
-    faultReason: order.faultReason || '',
-    handler: order.assignedTo || '',
-    contact: order.contact || '',
-    processMeasure: order.processMeasure || ''
-  }
-  modalUploadedFiles.value = order.attachments || []
-  processModalVisible.value = true
-  selectedCompletedOrder.value = null
+// ========== 用户映射(接单人 ID → 姓名) ==========
+const userMap = ref({})
+function userNames(ids) {
+  if (!ids) return ''
+  return String(ids).split(',').map(id => userMap.value[id] || id).join('、')
+}
+async function loadUsers() {
+  try {
+    const res = await listUser({ pageNum: 1, pageSize: 200 })
+    if (res.rows) {
+      userMap.value = Object.fromEntries(res.rows.map(u => [u.userId, u.nickName || u.userName]))
+    }
+  } catch(e) { console.warn('加载用户列表失败', e) }
 }
 
-const closeProcessModal = () => {
-  processModalVisible.value = false
-  currentOrder.value = null
+// ========== 派单(部门树 + 用户多选,参考排水/燃气工单) ==========
+const dispatchDialogVisible = ref(false)
+const dispatchForm = reactive({ handlerIds: [], remarks: '' })
+const deptTreeRef = ref(null)
+const userTableRef = ref(null)
+const deptTreeData = ref([])
+const deptFilterText = ref('')
+const userList = ref([])
+const selectedUserList = ref([])
+const submitting = ref(false)
+
+watch(deptFilterText, (val) => { deptTreeRef.value && deptTreeRef.value.filter(val) })
+function filterDeptNode(value, data) {
+  if (!value) return true
+  return data.deptName.includes(value)
 }
 
-// 处理附件上传(右侧面板)
-const handleFileUpload = (event) => {
-  const files = Array.from(event.target.files)
-  files.forEach(file => {
-    const reader = new FileReader()
-    reader.onload = (e) => {
-      uploadedFiles.value.push({
-        name: file.name,
-        size: file.size,
-        type: file.type,
-        preview: file.type.startsWith('image/') ? e.target.result : null,
-        file: file
-      })
-    }
-    reader.readAsDataURL(file)
-  })
+async function loadDeptTree() {
+  if (deptTreeData.value.length > 0) return
+  try {
+    const res = await listDept()
+    deptTreeData.value = handleTree(res.data || [], 'deptId')
+  } catch(e) { console.warn('加载部门失败', e) }
 }
 
-const removeFile = (idx) => {
-  uploadedFiles.value.splice(idx, 1)
+async function handleDeptClick(node) {
+  try {
+    userTableRef.value && userTableRef.value.clearSelection()
+    selectedUserList.value = []
+    dispatchForm.handlerIds = []
+    const res = await listUser({ deptId: node.deptId, pageNum: 1, pageSize: 100 })
+    userList.value = res.rows || []
+  } catch(e) { console.warn('加载部门用户失败', e) }
 }
 
-// 处理附件上传(模态框)
-const handleModalFileUpload = (event) => {
-  const files = Array.from(event.target.files)
-  files.forEach(file => {
-    modalUploadedFiles.value.push({
-      name: file.name,
-      size: file.size,
-      type: file.type,
-      file: file
-    })
-  })
+function handleSelectionChange(selection) {
+  selectedUserList.value = selection
+  dispatchForm.handlerIds = selection.map(item => item.userId)
 }
 
-const removeModalFile = (idx) => {
-  modalUploadedFiles.value.splice(idx, 1)
+function handleRowClick(row) {
+  userTableRef.value && userTableRef.value.toggleRowSelection(row)
 }
 
-// 提交处理(右侧面板)
-const submitProcess = () => {
-  if (!processData.value.faultType) {
-    alert('请选择故障类型')
-    return
-  }
-  if (!processData.value.processTime) {
-    alert('请填写处理时间')
+const selectedUserNames = computed(() => selectedUserList.value.map(item => item.nickName || item.userName))
+
+function handleDispatch() {
+  if (!canOperate.dispatch(selectedOrder.value)) {
+    ElMessage.warning('仅可对待派单工单进行派单操作')
     return
   }
-  if (!processData.value.faultReason) {
-    alert('请填写故障原因')
+  Object.assign(dispatchForm, { handlerIds: [], remarks: '' })
+  selectedUserList.value = []
+  userList.value = []
+  loadDeptTree()
+  dispatchDialogVisible.value = true
+}
+
+async function submitDispatch() {
+  if (!dispatchForm.handlerIds || dispatchForm.handlerIds.length === 0) {
+    ElMessage.warning('请选择至少一个接单人')
     return
   }
-  if (!processData.value.handler) {
-    alert('请填写处理人员')
+  submitting.value = true
+  try {
+    await dispatchWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      handlerIds: dispatchForm.handlerIds,
+      remarks: dispatchForm.remarks
+    })
+    ElMessage.success('派单成功!已向 ' + dispatchForm.handlerIds.length + ' 人下发任务')
+    dispatchDialogVisible.value = false
+    handleSearch()
+    if (selectedOrderId.value) handleSelectOrder(tableData.value.find(o => o.orderId === selectedOrderId.value) || {})
+  } catch(e) { ElMessage.error('派单失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+  finally { submitting.value = false }
+}
+
+// ========== 核查/处理(参考排水/燃气工单) ==========
+const actionDialogVisible = ref(false)
+const actionTitle = ref('')
+const actionFormData = reactive({ solution: '' })
+const processBeforeImages = ref('')
+const imageUploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload'
+
+function handleVerify() {
+  if (!canOperate.verify(selectedOrder.value)) {
+    ElMessage.warning('当前工单不允许核查')
     return
   }
-  if (!processData.value.contact) {
-    alert('请填写联系方式')
+  actionTitle.value = '核查工单'
+  actionFormData.solution = ''
+  processBeforeImages.value = ''
+  actionDialogVisible.value = true
+}
+
+function handleProcess() {
+  if (!canOperate.process(selectedOrder.value)) {
+    ElMessage.warning('当前工单不允许处理')
     return
   }
 
-  const order = currentProcessingOrder.value
-  order.status = 'completed'
-  order.faultType = processData.value.faultType
-  order.faultReason = processData.value.faultReason
-  order.processMeasure = processData.value.processMeasure
-  order.handler = processData.value.handler
-  order.contact = processData.value.contact
-  order.processTime = processData.value.processTime.replace('T', ' ')
-  order.completeTime = new Date().toLocaleString()
-  order.attachments = [...uploadedFiles.value]
-
-  alert(`工单 ${order.orderNo} 处理完成!`)
-  currentProcessingOrder.value = null
-  selectedCompletedOrder.value = order
+  ElMessageBox.alert(
+    '<div style="padding: 10px; color: #666;">' +
+    '<strong>维修操作指引:</strong><br/><br/>' +
+    '• 在下方表格中可以上传处理前的照片(最多9张)<br/>' +
+    '• 请选择本地图片或拍照上传<br/>' +
+    '• 填写处理说明后点击"确定"提交<br/>' +
+    '</div>',
+    '开始维修',
+    {
+      dangerouslyUseHTMLString: true,
+      confirmButtonText: '我已了解',
+      center: false
+    }
+  ).then(() => {
+    actionTitle.value = '处理工单'
+    actionFormData.solution = ''
+    processBeforeImages.value = ''
+    actionDialogVisible.value = true
+  }).catch(() => {})
 }
 
-// 提交处理(模态框)
-const submitModalProcess = () => {
-  if (!modalProcessData.value.faultType) {
-    alert('请选择故障类型')
-    return
-  }
-  if (!modalProcessData.value.processTime) {
-    alert('请填写处理时间')
+async function submitAction() {
+  if (!actionFormData.solution.trim()) {
+    ElMessage.warning('请填写说明')
     return
   }
-  if (!modalProcessData.value.faultReason) {
-    alert('请填写故障原因')
-    return
-  }
-  if (!modalProcessData.value.handler) {
-    alert('请填写处理人员')
+  submitting.value = true
+  try {
+    const data = {
+      orderId: selectedOrder.value.orderId,
+      handleResult: 'solved',
+      solution: actionFormData.solution
+    }
+    if (actionTitle.value === '处理工单' && processBeforeImages.value) {
+      data.maintenanceBeforeImg = processBeforeImages.value
+    }
+    if (actionTitle.value === '核查工单') {
+      await verifyWorkOrder(data)
+    } else {
+      await processWorkOrder(data)
+    }
+    ElMessage.success(actionTitle.value + '成功')
+    actionDialogVisible.value = false
+    handleSearch()
+    if (selectedOrderId.value) handleSelectOrder(tableData.value.find(o => o.orderId === selectedOrderId.value) || {})
+  } catch(e) { ElMessage.error('操作失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+  finally { submitting.value = false }
+}
+
+// ========== 提交验收(后端 completeOrder 要求状态 4) ==========
+const completeDialogVisible = ref(false)
+const completeForm = reactive({ result: '' })
+const maintenanceAfterImages = ref('')
+
+function handleComplete() {
+  if (!canOperate.complete(selectedOrder.value)) {
+    ElMessage.warning('当前工单不允许提交验收')
     return
   }
-  if (!modalProcessData.value.contact) {
-    alert('请填写联系方式')
+  completeForm.result = ''
+  maintenanceAfterImages.value = ''
+  completeDialogVisible.value = true
+}
+
+async function submitComplete() {
+  if (!completeForm.result.trim()) {
+    ElMessage.warning('请填写验收意见')
     return
   }
-
-  const order = currentOrder.value
-  order.status = 'completed'
-  order.faultType = modalProcessData.value.faultType
-  order.faultReason = modalProcessData.value.faultReason
-  order.processMeasure = modalProcessData.value.processMeasure
-  order.handler = modalProcessData.value.handler
-  order.contact = modalProcessData.value.contact
-  order.processTime = modalProcessData.value.processTime.replace('T', ' ')
-  order.completeTime = new Date().toLocaleString()
-  order.attachments = [...modalUploadedFiles.value]
-
-  alert(`工单 ${order.orderNo} 处理完成!`)
-  closeProcessModal()
-  currentProcessingOrder.value = null
-  selectedCompletedOrder.value = order
+  submitting.value = true
+  try {
+    await completeWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      result: completeForm.result,
+      maintenanceAfterImg: maintenanceAfterImages.value
+    })
+    ElMessage.success('提交验收成功')
+    completeDialogVisible.value = false
+    handleSearch()
+    if (selectedOrderId.value) handleSelectOrder(tableData.value.find(o => o.orderId === selectedOrderId.value) || {})
+  } catch(e) { ElMessage.error('提交失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+  finally { submitting.value = false }
 }
 
-const cancelProcess = () => {
-  currentProcessingOrder.value = null
+// ========== 验收通过/结案(参考排水/燃气工单) ==========
+const closeDialogVisible = ref(false)
+const closeForm = reactive({ result: '' })
+
+function handleClose() {
+  if (!canOperate.close(selectedOrder.value)) {
+    ElMessage.warning('当前工单不允许验收通过')
+    return
+  }
+  closeForm.result = ''
+  closeDialogVisible.value = true
 }
 
-// 查看工单详情
-const viewOrderDetail = (order) => {
-  if (order.status === 'completed') {
-    selectedCompletedOrder.value = order
-    currentProcessingOrder.value = null
-  } else {
-    alert(`工单状态:${getStatusText(order.status)}`)
+async function submitClose() {
+  if (!closeForm.result.trim()) {
+    ElMessage.warning('请填写验收结果')
+    return
   }
+  submitting.value = true
+  try {
+    await closeWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      result: closeForm.result
+    })
+    ElMessage.success('验收通过,工单已结案')
+    closeDialogVisible.value = false
+    handleSearch()
+    if (selectedOrderId.value) handleSelectOrder(tableData.value.find(o => o.orderId === selectedOrderId.value) || {})
+  } catch(e) { ElMessage.error('验收失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+  finally { submitting.value = false }
 }
 
-// 搜索
-const searchOrders = () => {
-  currentPage.value = 1
+// ========== 取消工单 ==========
+function handleCancel() {
+  ElMessageBox.confirm(`确定取消工单「${selectedOrder.value.orderNo}」吗?`, '取消工单', { type: 'warning' })
+    .then(async () => {
+      try {
+        await cancelWorkOrder(selectedOrder.value.orderId)
+        ElMessage.success('工单已取消')
+        selectedOrderId.value = null
+        selectedOrder.value = {}
+        handleSearch()
+      } catch(e) { ElMessage.error('取消失败:' + (e.msg || e.message || '请重试')); console.error(e) }
+    }).catch(() => {})
 }
 
-// 初始化默认选中第一个待处理工单
 onMounted(() => {
-  const pendingOrder = orders.value.find(o => o.status === 'pending')
-  if (pendingOrder) {
-    // 不自动开始处理,只是展示
-  }
+  loadUsers()
+  handleSearch()
 })
 </script>
 
-<style scoped>
-* { margin: 0; padding: 0; box-sizing: border-box; }
-
-.prototype-container {
-  max-width: 1600px;
-  margin: 0 auto;
-  background: #f0f4f8;
-  border-radius: 28px;
-  box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.15);
-  overflow: hidden;
-  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
-}
+<style scoped lang="scss">
+.order-container {
+  padding: 0 16px 16px;
 
-/* 头部 */
-.header {
-  background: #0a2b3c;
-  padding: 16px 28px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 16px;
-  border-bottom: 3px solid #e67e22;
-}
+  .search-card {
+    margin-bottom: 16px;
+
+    :deep(.el-card__body) {
+      padding: 16px;
+    }
+  }
+
+  .search-form {
+    .el-form {
+      display: flex;
+      flex-wrap: wrap;
+      align-items: flex-start;
+      gap: 12px;
+
+      .el-form-item {
+        margin-bottom: 0;
+        min-width: 150px;
+
+        &:last-child {
+          margin-left: auto;
+        }
+      }
+    }
+  }
+
+  .main-row {
+    height: calc(100vh - 200px);
+
+    .col-fill {
+      height: 100%;
+    }
+  }
+
+  .box-card {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+
+    :deep(.el-card__body) {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      overflow: hidden;
+    }
+  }
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  .list-scroll {
+    flex: 1;
+    overflow: hidden;
+  }
+
+  .order-list {
+    padding-right: 4px;
+  }
+
+  .order-item {
+    padding: 16px;
+    margin-bottom: 8px;
+    border-radius: 8px;
+    cursor: pointer;
+    transition: all 0.2s;
+    border: 1px solid #e4e7ed;
 
-.logo h2 { color: white; font-weight: 600; font-size: 1.3rem; }
-.logo p { color: #bdd8e8; font-size: 0.7rem; }
-
-.header-stats { display: flex; gap: 12px; }
-.stat-card { background: rgba(255,255,255,0.15); padding: 6px 14px; border-radius: 40px; text-align: center; color: white; }
-.stat-card.pending { background: #f39c12; }
-.stat-card.processing { background: #3498db; }
-.stat-card.completed { background: #27ae60; }
-.stat-value { font-weight: 700; font-size: 1.2rem; margin-right: 6px; }
-.stat-label { font-size: 0.65rem; opacity: 0.9; }
-
-/* 布局 */
-.main-layout { display: flex; flex-wrap: wrap; }
-.left-panel { width: 500px; background: #f9fbfd; padding: 20px; }
-.right-panel { flex: 1; background: #ffffff; padding: 20px; min-width: 480px; }
-
-/* 卡片 */
-.card { background: white; border-radius: 20px; border: 1px solid #e6edf3; margin-bottom: 20px; overflow: hidden; }
-.card-header { background: #f9fafc; padding: 14px 18px; border-bottom: 1px solid #eef2f6; font-weight: 700; font-size: 0.85rem; color: #1d5a78; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; }
-.card-body { padding: 16px; }
-
-/* 筛选栏 */
-.filter-bar { display: flex; gap: 10px; align-items: center; }
-.filter-select, .search-input { padding: 6px 12px; border-radius: 20px; border: 1px solid #cbdde6; font-size: 0.7rem; }
-.search-input { width: 150px; }
-.btn-primary, .btn-outline, .btn-sm { padding: 5px 12px; border-radius: 30px; font-size: 0.7rem; cursor: pointer; }
-.btn-primary { background: #e67e22; border: none; color: white; }
-.btn-primary:hover { background: #d35400; }
-.btn-outline { background: white; border: 1px solid #e67e22; color: #e67e22; }
-
-/* 工单列表 */
-.order-list { max-height: 550px; overflow-y: auto; }
-.order-item { background: white; border-radius: 16px; padding: 14px; margin-bottom: 12px; border: 1px solid #eef2f6; transition: 0.2s; }
-.order-item.pending { border-left: 4px solid #f39c12; }
-.order-item.processing { border-left: 4px solid #3498db; background: #f0f8ff; }
-.order-item.completed { border-left: 4px solid #27ae60; background: #f0fff4; }
-.order-header { display: flex; justify-content: space-between; margin-bottom: 8px; }
-.order-id { display: flex; gap: 10px; align-items: center; }
-.order-no { font-family: monospace; font-weight: 700; font-size: 0.8rem; color: #e67e22; }
-.order-type { font-size: 0.6rem; padding: 2px 8px; border-radius: 20px; }
-.type-urgent { background: #fee2e2; color: #e74c3c; }
-.type-normal { background: #e3f2fd; color: #3498db; }
-.order-status { font-size: 0.65rem; padding: 2px 10px; border-radius: 20px; }
-.status-pending { background: #fef3c7; color: #f39c12; }
-.status-processing { background: #e3f2fd; color: #3498db; }
-.status-completed { background: #d5f5e3; color: #27ae60; }
-.order-device, .order-desc, .order-time { font-size: 0.7rem; margin-bottom: 6px; }
-.label { color: #6f8ea8; margin-right: 4px; }
-.value { color: #2c5a72; }
-.order-actions { display: flex; gap: 8px; margin-top: 10px; }
-.btn-start { background: #27ae60; color: white; border: none; padding: 4px 12px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-.btn-process { background: #3498db; color: white; border: none; padding: 4px 12px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-.btn-view { background: #95a5a6; color: white; border: none; padding: 4px 12px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
-.empty-order { text-align: center; padding: 40px; color: #8aaec2; }
-
-/* 分页 */
-.pagination { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
-.page-btn { background: white; border: 1px solid #cbdde6; padding: 4px 10px; border-radius: 20px; font-size: 0.7rem; cursor: pointer; }
-.page-btn.active { background: #e67e22; color: white; border-color: #e67e22; }
-
-/* 处理面板 */
-.process-info { background: #f8fafc; border-radius: 16px; padding: 12px; margin-bottom: 20px; }
-.process-info .info-row { font-size: 0.75rem; margin-bottom: 6px; }
-.process-info .label { font-weight: 600; width: 80px; display: inline-block; }
-.process-form { margin-top: 8px; }
-.form-title { font-weight: 600; font-size: 0.8rem; color: #2c5a72; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 1px solid #eef2f6; }
-.form-row { display: flex; gap: 12px; margin-bottom: 12px; }
-.form-group { margin-bottom: 12px; }
-.form-group.half { flex: 1; }
-.form-label { font-size: 0.7rem; font-weight: 600; margin-bottom: 4px; display: block; color: #2c5a72; }
-.form-input, .form-select, .form-textarea { width: 100%; padding: 8px 12px; border-radius: 12px; border: 1px solid #cbdde6; font-size: 0.75rem; }
-.form-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px; }
-
-/* 上传区域 */
-.upload-area { border: 1px dashed #cbdde6; border-radius: 12px; padding: 12px; background: #fafcfd; }
-.upload-btn { background: #3498db; color: white; padding: 6px 14px; border-radius: 30px; font-size: 0.7rem; cursor: pointer; display: inline-block; }
-.file-list { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 12px; }
-.file-item { display: flex; flex-direction: column; align-items: center; width: 80px; background: #f8fafc; border-radius: 8px; padding: 8px; position: relative; }
-.file-preview { width: 60px; height: 60px; object-fit: cover; border-radius: 8px; margin-bottom: 4px; }
-.file-name { font-size: 0.6rem; color: #2c5a72; text-align: center; word-break: break-all; }
-.remove-file { position: absolute; top: -6px; right: -6px; background: #e74c3c; color: white; border: none; border-radius: 50%; width: 18px; height: 18px; font-size: 10px; cursor: pointer; }
-.upload-hint { font-size: 0.6rem; color: #8aaec2; margin-top: 8px; }
-.file-tag { background: #eef2fa; padding: 4px 8px; border-radius: 20px; font-size: 0.65rem; display: inline-flex; align-items: center; gap: 6px; }
-
-/* 已完成工单详情 */
-.completed-info { padding: 8px; }
-.info-group { margin-bottom: 20px; }
-.group-title { font-weight: 600; font-size: 0.75rem; color: #2c5a72; margin-bottom: 10px; padding-bottom: 4px; border-bottom: 1px solid #eef2f6; }
-.info-row { font-size: 0.7rem; margin-bottom: 8px; }
-.info-row .label { font-weight: 600; width: 80px; display: inline-block; color: #6f8ea8; }
-.attachment-list { display: flex; flex-wrap: wrap; gap: 12px; }
-.attachment-item { display: flex; flex-direction: column; align-items: center; width: 80px; }
-.attachment-img { width: 70px; height: 70px; object-fit: cover; border-radius: 8px; border: 1px solid #eef2f6; }
-.attachment-name { font-size: 0.6rem; color: #6f8ea8; margin-top: 4px; text-align: center; }
-
-.empty-process { text-align: center; padding: 60px 20px; color: #8aaec2; }
-.empty-icon { font-size: 3rem; margin-bottom: 12px; opacity: 0.5; }
-.empty-hint { font-size: 0.65rem; margin-top: 8px; }
-.order-badge { font-size: 0.65rem; background: #eef2fa; padding: 2px 10px; border-radius: 20px; font-weight: normal; }
-
-/* 模态框 */
-.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
-.modal-content { background: white; border-radius: 24px; width: 550px; max-width: 90%; padding: 24px; max-height: 85vh; overflow-y: auto; }
-.order-summary { background: #f8fafc; border-radius: 12px; padding: 12px; margin-bottom: 16px; font-size: 0.7rem; }
-.process-form-modal { margin-top: 8px; }
-</style>
+    &:hover {
+      background-color: #f5f7fa;
+    }
+
+    &.active {
+      background-color: #ecf5ff;
+      border-color: #409eff;
+    }
+
+    .order-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 8px;
+
+      .order-no {
+        font-weight: bold;
+        font-size: 14px;
+      }
+
+      .status-badge {
+        min-width: 60px;
+      }
+    }
+
+    .order-body {
+      font-size: 12px;
+      color: #606266;
+
+      .order-tags {
+        display: flex;
+        gap: 6px;
+        margin-bottom: 6px;
+      }
+
+      .order-desc {
+        margin-top: 4px;
+        color: #303133;
+        font-size: 13px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+
+      .device-code, .create-time {
+        margin-top: 4px;
+      }
+    }
+  }
+
+  .pagination-wrapper {
+    padding: 16px 0;
+    border-top: 1px solid #e4e7ed;
+    margin-top: 8px;
+
+    .pagination {
+      display: flex;
+      justify-content: flex-end;
+    }
+  }
+
+  .detail-body {
+    flex: 1;
+    overflow-y: auto;
+  }
+
+  .detail-tabs {
+    margin-top: 0;
+  }
+
+  .timeline-scroll {
+    max-height: calc(100vh - 340px);
+    overflow-y: auto;
+    padding-right: 8px;
+  }
+
+  .device-section, .image-section, .action-section {
+    margin-top: 24px;
+    padding-top: 24px;
+    border-top: 1px solid #e4e7ed;
+
+    .section-title {
+      font-size: 15px;
+      font-weight: bold;
+      margin-bottom: 16px;
+      color: #303133;
+    }
+  }
+
+  .image-list {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+  }
+
+  .image-group + .image-group {
+    margin-top: 16px;
+  }
+
+  .image-label {
+    display: block;
+    margin-bottom: 8px;
+    color: #606266;
+    font-size: 13px;
+  }
+
+  .action-buttons {
+    display: flex;
+    gap: 12px;
+    flex-wrap: wrap;
+
+    .el-button {
+      min-width: 100px;
+    }
+  }
+
+  .timeline-content {
+    .timestamp {
+      font-size: 12px;
+      color: #909399;
+      margin-bottom: 6px;
+    }
+
+    .operator-info {
+      font-size: 14px;
+      margin-bottom: 8px;
+
+      .operator-name {
+        font-weight: bold;
+        color: #303133;
+      }
+
+      .oper-type {
+        color: #909399;
+      }
+    }
+
+    .oper-desc {
+      font-size: 14px;
+      color: #606266;
+    }
+  }
+}
+</style>

+ 122 - 0
src/views/subSystem/pipeNetwork/pworkOrder/workOrderDict.js

@@ -0,0 +1,122 @@
+// ========== 工单业务公共字典 ==========
+// 数据源:/api/work-order/* (BusinessWorkOrderController)
+
+// 工单类型:1-故障维修 2-日常巡检 3-设备保养
+export const ORDER_TYPE_MAP = {
+  1: { text: '故障维修', type: 'danger' },
+  2: { text: '日常巡检', type: 'primary' },
+  3: { text: '设备保养', type: 'warning' }
+}
+
+// 工单优先级:1-紧急 2-一般 3-低
+export const ORDER_LEVEL_MAP = {
+  1: { text: '紧急', type: 'danger' },
+  2: { text: '一般', type: 'warning' },
+  3: { text: '低', type: 'info' }
+}
+
+// 工单状态:1-待派单 2-待接单 3-已接单 4-处理中 5-待验收 6-已结案 7-已驳回 8-延期审核 9-已延期
+export const ORDER_STATUS_MAP = {
+  1: { text: '待派单', type: 'danger' },
+  2: { text: '待接单', type: 'warning' },
+  3: { text: '已接单', type: 'primary' },
+  4: { text: '处理中', type: 'warning' },
+  5: { text: '待验收', type: 'primary' },
+  6: { text: '已结案', type: 'success' },
+  7: { text: '已驳回', type: 'info' },
+  8: { text: '延期审核', type: 'warning' },
+  9: { text: '已延期', type: 'warning' }
+}
+
+// 业务简化为三态分组(统计卡用)
+export const ORDER_STATUS_GROUP = {
+  pending: [1, 2, 3],       // 待处理:待派单/待接单/已接单
+  processing: [4, 8, 9],    // 处理中:处理中/延期审核/已延期
+  completed: [5, 6],        // 已完成:待验收/已结案
+  rejected: [7]             // 已驳回
+}
+
+// 工单操作日志类型(后端 WorkOrderLog.operType 数字枚举)
+export const ORDER_LOG_TYPE_MAP = {
+  1: { text: '创建工单', type: 'primary' },
+  2: { text: '派单', type: 'warning' },
+  3: { text: '接单', type: 'success' },
+  4: { text: '开始维修', type: 'warning' },
+  5: { text: '提交验收', type: 'primary' },
+  6: { text: '验收通过', type: 'success' },
+  7: { text: '驳回工单', type: 'info' },
+  8: { text: '申请延期', type: 'warning' },
+  9: { text: '延期通过', type: 'warning' }
+}
+
+// ========== 辅助函数 ==========
+
+/** 工单状态文本 */
+export function orderStatusText(status) {
+  return ORDER_STATUS_MAP[status]?.text || '未知'
+}
+
+/** 工单状态 Tag type */
+export function orderStatusType(status) {
+  return ORDER_STATUS_MAP[status]?.type || 'info'
+}
+
+/** 工单类型文本 */
+export function orderTypeText(type) {
+  return ORDER_TYPE_MAP[type]?.text || '未知'
+}
+
+/** 工单类型 Tag type */
+export function orderTypeTagType(type) {
+  return ORDER_TYPE_MAP[type]?.type || 'info'
+}
+
+/** 优先级文本 */
+export function orderLevelText(level) {
+  return ORDER_LEVEL_MAP[level]?.text || '-'
+}
+
+/** 优先级 Tag type */
+export function orderLevelType(level) {
+  return ORDER_LEVEL_MAP[level]?.type || 'info'
+}
+
+/** 是否属于某状态分组 */
+export function inStatusGroup(order, groupName) {
+  const list = ORDER_STATUS_GROUP[groupName] || []
+  return list.includes(Number(order?.orderStatus))
+}
+
+/** 是否允许该操作(状态机与后端 BusinessWorkOrderServiceImpl 对齐)
+ * 流程:1待派单 →2待接单 →3已接单 →4处理中 →5待验收 →6已结案
+ * 动作:派单(1) → 核查(2/3) → 处理(3/4) → 提交验收(4) → 验收通过(5);任意非终态可取消(→7)
+ */
+export const canOperate = {
+  /** 待派单可派单 */
+  dispatch: (o) => Number(o?.orderStatus) === 1,
+  /** 待接单/已接单可核查 */
+  verify: (o) => [2, 3].includes(Number(o?.orderStatus)),
+  /** 已接单/处理中可处理 */
+  process: (o) => [3, 4].includes(Number(o?.orderStatus)),
+  /** 处理中可提交验收 */
+  complete: (o) => Number(o?.orderStatus) === 4,
+  /** 待验收可验收通过/结案 */
+  close: (o) => Number(o?.orderStatus) === 5,
+  /** 可取消:未结案/未驳回 */
+  cancel: (o) => ![6, 7].includes(Number(o?.orderStatus))
+}
+
+/** 工单操作日志文本 */
+export function orderLogText(type) {
+  return ORDER_LOG_TYPE_MAP[type]?.text || type || '操作'
+}
+
+/** 工单操作日志 Tag type */
+export function orderLogType(type) {
+  return ORDER_LOG_TYPE_MAP[type]?.type || 'info'
+}
+
+/** 时间兜底显示 */
+export function timeText(t) {
+  return t || '-'
+}

+ 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/, '')

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików