1
0

12 Commity 28f1c8c26c ... 2e1b2e4849

Autor SHA1 Správa Dátum
  nahida 2e1b2e4849 Merge remote-tracking branch 'origin/master' 1 týždeň pred
  nahida 9dae7f2fc3 feat(build): 集成 UnoCSS 样式引擎 1 týždeň pred
  nahida 0449f85d00 refactor(manholeCover): 重构工单处置页面提升用户体验 1 týždeň pred
  nahida f873cb5ba8 feat(manholeCover): 重构井盖报警页面功能 1 týždeň pred
  nahida 8e73ee2487 refactor(manholeCover): 重构井盖工单处理页面 1 týždeň pred
  nahida 38a1e633f6 refactor(manholeCover): 重构办结工单页面实现真实数据交互 1 týždeň pred
  nahida 0a3f8e3574 refactor(manholeCover): 重构井盖案件流转页面 1 týždeň pred
  nahida 77451738bb feat(pipeNetwork): 添加井盖监测设备管理和工单处理功能 1 týždeň pred
  nahida 20562b65b1 feat(manholeCover): 重构井盖报警态势分析页面 1 týždeň pred
  nahida 10574380ee refactor(manholeCover): 重构井盖报警列表页面数据处理逻辑 1 týždeň pred
  nahida 2ebae40d07 feat(config): 添加百度地图AK配置和UnoCSS依赖 1 týždeň pred
  nahida 7b529fc95e feat(case): 支持GET和POST方式查询案件信息列表 1 týždeň pred

+ 3 - 0
.env.development

@@ -9,3 +9,6 @@ VITE_APP_BASE_API = '/dev-api'
 
 # MinIO文件访问基础地址
 VITE_APP_MINIO_BASE_URL = 'http://111.23.174.45:9000/pipe/'
+
+# 百度地图 WebGL AK
+VITE_BAIDU_MAP_AK = 'vMma3LYQQyrfhlFUm0CFzXFQhBrwxAmi'

+ 5 - 0
package.json

@@ -34,8 +34,13 @@
     "vue-router": "4.4.0"
   },
   "devDependencies": {
+    "@unocss/preset-attributify": "^66.7.4",
+    "@unocss/preset-uno": "^66.7.4",
+    "@unocss/transformer-directives": "^66.7.4",
+    "@unocss/vite": "^66.7.4",
     "@vitejs/plugin-vue": "5.0.5",
     "sass": "^1.77.5",
+    "unocss": "^66.7.4",
     "unplugin-auto-import": "0.17.6",
     "unplugin-vue-setup-extend-plus": "1.0.1",
     "vite": "5.3.2",

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

@@ -60,6 +60,66 @@ export function deleteManholeDeviceRel(relId) {
   return request({ url: '/api/manhole/device/delete', method: 'delete', params: { relId } })
 }
 
+// ============ 井盖监测设备阈值 ============
+export function getManholeAlarmDeviceList(data = {}) {
+  return request({ url: '/manhole/device/getManholeDataList', method: 'post', data })
+}
+export function getManholeAlarmDeviceDetail(id) {
+  return request({ url: '/manhole/device/getById/' + id, method: 'get' })
+}
+export function saveManholeAlarmThreshold(data) {
+  return request({ url: '/manhole/device/saveWarningThreshold', method: 'post', data })
+}
+
+export function getManholeTerminalFaultPage(data) {
+  return request({ url: '/manhole/terminalFault/findByPage', method: 'post', data })
+}
+
+export function getManholeTerminalFaultDetail(id) {
+  return request({ url: '/manhole/terminalFault/getById/' + id, method: 'get' })
+}
+
+export function handleManholeTerminalFault(data) {
+  return request({ url: '/manhole/terminalFault/handleFault', method: 'post', data })
+}
+
+// ============ 井盖案件流转 / 工单处置 ============
+export function getManholeCasePage(data) {
+  return request({ url: '/manhole/caseInfo/findByPage', method: 'post', data })
+}
+
+export function getManholeCaseDetail(id) {
+  return request({ url: '/manhole/caseInfo/getById/' + id, method: 'get' })
+}
+
+export function verifyManholeCase(data) {
+  return request({ url: '/manhole/caseInfo/verifyCaseInfo', method: 'post', data })
+}
+
+export function getManholeWorkOrderPage(data) {
+  return request({ url: '/manhole/workOrder/findByPage', method: 'post', data })
+}
+
+export function getManholeWorkOrderProcessingPage(data) {
+  return request({ url: '/manhole/workOrder/findProcessingPage', method: 'post', data })
+}
+
+export function getManholeWorkOrderFinishedPage(data) {
+  return request({ url: '/manhole/workOrder/findFinishedPage', method: 'post', data })
+}
+
+export function getManholeWorkOrderDetail(id) {
+  return request({ url: '/manhole/workOrder/getById/' + id, method: 'get' })
+}
+
+export function addManholeWorkOrder(data) {
+  return request({ url: '/manhole/workOrder/addWorkOrder', method: 'post', data })
+}
+
+export function operManholeWorkOrder(data) {
+  return request({ url: '/manhole/workOrder/operWorkOrder', method: 'post', data })
+}
+
 // ============ 维修记录 ============
 export function getRepairRecordPage(pageNum, pageSize, params) {
   return request({ url: '/api/repair-record/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })

+ 5 - 7
src/main.js

@@ -1,4 +1,4 @@
-import { createApp } from 'vue'
+import {createApp} from 'vue'
 
 import Cookies from 'js-cookie'
 
@@ -7,15 +7,14 @@ import 'element-plus/dist/index.css'
 import locale from 'element-plus/es/locale/lang/zh-cn'
 
 import '@/assets/styles/index.scss' // global css
-
+import 'virtual:uno.css' // uno css
 import App from './App'
 import store from './store'
 import router from './router'
 import directive from './directive' // directive
-
 // 注册指令
 import plugins from './plugins' // plugins
-import { download } from '@/utils/request'
+import {download} from '@/utils/request'
 
 // svg图标
 import 'virtual:svg-icons-register'
@@ -23,9 +22,8 @@ import SvgIcon from '@/components/SvgIcon'
 import elementIcons from '@/components/SvgIcon/svgicon'
 
 import './permission' // permission control
-
-import { useDict } from '@/utils/dict'
-import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
+import {useDict} from '@/utils/dict'
+import {addDateRange, handleTree, parseTime, resetForm, selectDictLabel, selectDictLabels} from '@/utils/ruoyi'
 
 // 分页组件
 import Pagination from '@/components/Pagination'

+ 214 - 205
src/views/subSystem/manholeCover/alarm/alarmList.vue

@@ -18,17 +18,13 @@
           <div class="stat-num">{{ warningCount }}</div>
           <div class="stat-label">一般报警</div>
         </div>
-        <div class="stat-item resolved" @click="filterAlerts('resolved')">
-          <div class="stat-num">{{ resolvedCount }}</div>
-          <div class="stat-label">已处置</div>
-        </div>
       </div>
     </div>
 
     <!-- 主内容区域:左侧报警列表 + 右侧详情研判 -->
     <div class="main-content">
       <!-- 左侧报警列表 -->
-      <div class="alert-list-panel">
+      <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" />
@@ -71,20 +67,16 @@
               <el-tag type="danger" size="small">待处理</el-tag>
             </div>
           </div>
+          <el-empty v-if="!filteredAlerts.length && !listLoading" description="暂无报警事件" :image-size="100" />
         </div>
       </div>
 
       <!-- 右侧报警详情与研判面板 -->
-      <div class="detail-panel" v-if="selectedAlert">
+      <div class="detail-panel" v-if="selectedAlert" v-loading="detailLoading">
         <!-- 报警基本信息 -->
         <div class="detail-section">
           <div class="section-title">
             <span><el-icon><Warning /></el-icon> 报警详情 - 辅助研判</span>
-            <div class="action-buttons">
-              <el-button v-if="selectedAlert.status === 'pending'" type="primary" size="small" @click="startProcess">开始处理</el-button>
-              <el-button v-if="selectedAlert.status !== 'resolved'" type="success" size="small" @click="resolveAsTrue">确认为真报警</el-button>
-              <el-button v-if="selectedAlert.status !== 'resolved'" type="info" size="small" @click="resolveAsFalse">误报警排除</el-button>
-            </div>
           </div>
           <el-descriptions :column="2" border>
             <el-descriptions-item label="报警位置">
@@ -110,67 +102,6 @@
             </el-descriptions-item>
           </el-descriptions>
         </div>
-
-        <!-- 实时监测曲线 (帮助分析异常趋势) -->
-        <div class="detail-section">
-          <div class="section-title">
-            <span><el-icon><TrendCharts /></el-icon> 实时监测数据趋势(近1小时)</span>
-            <span class="sub-hint">辅助综合研判,排除误报</span>
-          </div>
-          <div class="chart-container">
-            <v-chart :option="realtimeChartOption" autoresize />
-          </div>
-          <div class="analysis-tips">
-            <el-alert
-                :title="analysisSuggestion"
-                type="info"
-                show-icon
-                :closable="false"
-                description="结合历史数据和当前环境因素,辅助判断报警真实性。"
-            />
-          </div>
-        </div>
-
-        <!-- 处置记录与研判日志 -->
-        <div class="detail-section">
-          <div class="section-title">
-            <span><el-icon><Document /></el-icon> 处置记录 & 研判日志</span>
-            <el-button size="small" link @click="addNote">添加备注</el-button>
-          </div>
-          <el-timeline>
-            <el-timeline-item
-                v-for="log in selectedAlert.logs"
-                :key="log.id"
-                :timestamp="log.time"
-                :type="log.type"
-                :hollow="true"
-            >
-              {{ log.content }}
-            </el-timeline-item>
-            <el-timeline-item v-if="!selectedAlert.logs?.length" timestamp="暂无处置记录" type="info">
-              系统已记录报警,待处理
-            </el-timeline-item>
-          </el-timeline>
-        </div>
-
-        <!-- 相似历史报警参考 -->
-        <div class="detail-section">
-          <div class="section-title">
-            <span><el-icon></el-icon> 相似历史报警参考</span>
-          </div>
-          <div class="history-ref">
-            <el-table :data="similarAlerts" size="small" style="width: 100%">
-              <el-table-column prop="time" label="报警时间" width="140" />
-              <el-table-column prop="alertType" label="报警类型" />
-              <el-table-column prop="realData" label="实时数据" width="100" />
-              <el-table-column prop="result" label="研判结果" width="100">
-                <template #default="{ row }">
-                  <el-tag :type="row.result === '真报警' ? 'danger' : 'info'" size="small">{{ row.result }}</el-tag>
-                </template>
-              </el-table-column>
-            </el-table>
-          </div>
-        </div>
       </div>
 
       <!-- 未选中报警时的空状态 -->
@@ -179,169 +110,250 @@
       </div>
     </div>
 
-    <!-- 地图弹窗(模拟GIS位置) -->
+    <!-- 地图弹窗 -->
     <el-dialog v-model="mapDialogVisible" title="报警位置地图" width="600px">
       <div id="simpleMap" style="height: 400px; background: #e9f5e9; border-radius: 12px; display: flex; align-items: center; justify-content: center; flex-direction: column;">
         <el-icon :size="48" color="#f56c6c"><Location /></el-icon>
         <div><strong>{{ selectedAlert?.location }}</strong></div>
-        <div>经度: {{ selectedAlert?.lng || '121.487' }}, 纬度: {{ selectedAlert?.lat || '31.249' }}</div>
-        <div style="margin-top: 20px; font-size: 12px; color: gray">(GIS模拟定位 - 报警点闪烁标记)</div>
+        <div>经度: {{ selectedAlert?.lng || '暂无数据' }}, 纬度: {{ selectedAlert?.lat || '暂无数据' }}</div>
+        <div style="margin-top: 20px; font-size: 12px; color: gray">当前仅展示接口返回的真实坐标信息</div>
       </div>
     </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
-import { BellFilled, WarningFilled, InfoFilled, CircleCheck, Location, Warning, TrendCharts, Document, CircleClose, Search } from '@element-plus/icons-vue'
-import { use } from 'echarts/core'
-import { CanvasRenderer } from 'echarts/renderers'
-import { LineChart } from 'echarts/charts'
-import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
-import { ElMessage } from 'element-plus'
+import {computed, onMounted, ref} from 'vue'
+import {CircleCheck, InfoFilled, Location, Warning, WarningFilled} from '@element-plus/icons-vue'
+import {ElMessage} from 'element-plus'
+import {getManholeAlarmDeviceDetail, getManholeAlarmDeviceList} from '@/api/pipeNetwork/basic'
 
-use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
-
-// 模拟报警数据
-const generateAlerts = () => [
-  {
-    id: 1, deviceId: 'MN-1001', deviceName: '人民路智能井盖', deviceType: '智能井盖监测仪',
-    location: '人民路与解放路口东50m', lng: 121.487, lat: 31.249,
-    alertType: '倾斜角度超限', alertParam: '倾斜角', realData: '12.5°', threshold: '8°', exceedValue: '12.5°',
-    level: 'critical', time: '2025-04-02 14:23:15', status: 'pending',
-    logs: [{ id: 1, time: '2025-04-02 14:23:20', content: '系统自动触发报警', type: 'danger' }]
-  },
-  {
-    id: 2, deviceId: 'MN-1002', deviceName: '滨江路防爆井盖', deviceType: '防爆井盖监测仪',
-    location: '滨江路化工园区南门', lng: 121.502, lat: 31.235,
-    alertType: '水位超限', alertParam: '水位深度', realData: '58cm', threshold: '40cm', exceedValue: '58cm',
-    level: 'critical', time: '2025-04-02 13:55:02', status: 'pending',
-    logs: [{ id: 1, time: '2025-04-02 13:55:10', content: '井下水位超阈值告警', type: 'danger' }]
-  },
-  {
-    id: 3, deviceId: 'MN-1003', deviceName: '古城街铸铁井盖', deviceType: '普通井盖监测仪',
-    location: '古城街北段树下', lng: 121.478, lat: 31.258,
-    alertType: '异常震动', alertParam: '震动强度', realData: '112mg', threshold: '80mg', exceedValue: '112mg',
-    level: 'warning', time: '2025-04-02 12:10:33', status: 'processing',
-    logs: [{ id: 1, time: '2025-04-02 12:15:00', content: '已派发巡检人员现场核实', type: 'warning' }]
-  },
-  {
-    id: 4, deviceId: 'MN-1005', deviceName: '南港路井盖', deviceType: '智能井盖监测仪',
-    location: '南港路旧车站段', lng: 121.495, lat: 31.228,
-    alertType: '井盖开启异常', alertParam: '开盖状态', realData: '开启', threshold: '关闭', exceedValue: '异常开启',
-    level: 'warning', time: '2025-04-02 11:45:20', status: 'pending',
-    logs: []
-  },
-  {
-    id: 5, deviceId: 'EX-3002', deviceName: '石化区防爆井盖', deviceType: '防爆井盖监测仪',
-    location: '石化大道南侧', lng: 121.524, lat: 31.239,
-    alertType: '气体浓度超标', alertParam: 'H₂S浓度', realData: '28ppm', threshold: '15ppm', exceedValue: '28ppm',
-    level: 'critical', time: '2025-04-02 10:30:45', status: 'resolved',
-    logs: [{ id: 1, time: '2025-04-02 11:00:00', content: '经现场确认,为真报警,已通风处置', type: 'success' }]
-  }
-]
-
-const alerts = ref(generateAlerts())
+const alerts = ref([])
 const searchText = ref('')
 const filterType = ref('all')
-const selectedAlert = ref(alerts.value[0])
+const selectedAlert = ref(null)
 const mapDialogVisible = ref(false)
+const listLoading = ref(false)
+const detailLoading = ref(false)
 
-const filteredAlerts = computed(() => {
-  let list = alerts.value
-  if (filterType.value === 'critical') list = list.filter(a => a.level === 'critical' && a.status !== 'resolved')
-  else if (filterType.value === 'warning') list = list.filter(a => a.level === 'warning' && a.status !== 'resolved')
-  else if (filterType.value === 'resolved') list = list.filter(a => a.status === 'resolved')
-  else list = list.filter(a => a.status !== 'resolved' || filterType.value === 'resolved' ? true : a.status !== 'resolved')
+function toNumber(value) {
+  const num = Number(value)
+  return Number.isFinite(num) ? num : null
+}
 
-  if (searchText.value) {
-    const kw = searchText.value.toLowerCase()
-    list = list.filter(a => a.location.toLowerCase().includes(kw) || a.deviceId.toLowerCase().includes(kw))
+function formatDateTime(value) {
+  if (!value) return '—'
+  return String(value).replace('T', ' ')
+}
+
+function getThresholdTextByCode(raw, warningCode) {
+  const list = Array.isArray(raw?.warningThresholdList) ? raw.warningThresholdList : []
+  const target = list.find(item => item.warningCode === warningCode)
+  if (!target) return '—'
+  const { minValue, maxValue } = target
+  if (minValue !== null && minValue !== undefined && maxValue !== null && maxValue !== undefined) {
+    return `${minValue} ~ ${maxValue}`
   }
-  return list
-})
+  if (minValue !== null && minValue !== undefined) return `<= ${minValue}`
+  if (maxValue !== null && maxValue !== undefined) return `>= ${maxValue}`
+  return '—'
+}
 
-const totalAlerts = computed(() => alerts.value.filter(a => a.status !== 'resolved').length)
-const criticalCount = computed(() => alerts.value.filter(a => a.level === 'critical' && a.status !== 'resolved').length)
-const warningCount = computed(() => alerts.value.filter(a => a.level === 'warning' && a.status !== 'resolved').length)
-const resolvedCount = computed(() => alerts.value.filter(a => a.status === 'resolved').length)
-const unhandledCount = computed(() => alerts.value.filter(a => a.status === 'pending').length)
+function buildAlertInfo(raw) {
+  const status = raw?.equipmentStatus || {}
+  const manholeData = raw?.manholeData || {}
+  const battery = toNumber(manholeData.batteryLevel)
+  const signal = toNumber(manholeData.signalStrength)
+  const tiltAngle = toNumber(manholeData.tiltAngle)
 
-const filterAlerts = (type) => { filterType.value = type }
+  if (`${manholeData.waterLevelAlarmStatus}` === '1') {
+    return {
+      hasAlert: true,
+      level: 'critical',
+      levelText: '严重报警',
+      alertType: '水位报警',
+      alertParam: '水位',
+      realData: manholeData.waterLevel !== undefined && manholeData.waterLevel !== null ? `${manholeData.waterLevel}cm` : '报警',
+      threshold: getThresholdTextByCode(raw, 'WARN-WATER-LEVEL')
+    }
+  }
 
-const selectAlert = (alert) => { selectedAlert.value = alert }
+  if (`${manholeData.waterInfiltrationAlarmStatus}` === '1') {
+    return {
+      hasAlert: true,
+      level: 'critical',
+      levelText: '严重报警',
+      alertType: '水浸报警',
+      alertParam: '水浸状态',
+      realData: '报警',
+      threshold: getThresholdTextByCode(raw, 'WARN-WATER-INFILTRATION')
+    }
+  }
 
-// 模拟实时数据曲线(基于报警类型生成模拟趋势数据)
-const realtimeChartOption = computed(() => {
-  if (!selectedAlert.value) return {}
-  const param = selectedAlert.value.alertParam || '数值'
-  const unit = selectedAlert.value.alertType.includes('角度') ? '°' : selectedAlert.value.alertType.includes('水位') ? 'cm' : selectedAlert.value.alertType.includes('震动') ? 'mg' : ''
-  // 模拟最近12个时间点的数据,最后一点为报警值突出
-  const baseValue = parseFloat(selectedAlert.value.realData) || 20
-  const thresholdVal = parseFloat(selectedAlert.value.threshold) || 10
-  const mockData = Array.from({ length: 12 }, (_, i) => {
-    if (i === 11) return baseValue
-    return baseValue - (Math.random() * (baseValue - thresholdVal) * 0.6 + 2)
-  })
-  const times = Array.from({ length: 12 }, (_, i) => `${i * 5}分钟前`).reverse()
-  return {
-    tooltip: { trigger: 'axis' },
-    xAxis: { type: 'category', data: times },
-    yAxis: { type: 'value', name: `${param} (${unit})` },
-    series: [
-      { name: '实时监测值', type: 'line', data: mockData, smooth: true, lineStyle: { color: '#f56c6c', width: 2 }, areaStyle: { opacity: 0.2 }, markLine: { data: [{ yAxis: thresholdVal, name: '阈值线', lineStyle: { color: '#e6a23c', type: 'dashed' } }] } }
-    ]
+  if (Number(status.alarmStatus) === 1) {
+    return {
+      hasAlert: true,
+      level: 'critical',
+      levelText: '严重报警',
+      alertType: tiltAngle !== null ? '倾斜报警' : '设备报警',
+      alertParam: tiltAngle !== null ? '倾斜角' : '设备状态',
+      realData: tiltAngle !== null ? `${tiltAngle}°` : '报警',
+      threshold: tiltAngle !== null ? getThresholdTextByCode(raw, 'WARN-TILT') : '—'
+    }
   }
-})
 
-const analysisSuggestion = computed(() => {
-  if (!selectedAlert.value) return ''
-  const alert = selectedAlert.value
-  if (alert.alertType.includes('倾斜')) return '当前倾斜角度超阈值12.5°,超过设定值50%以上,且近期有道路施工记录,建议现场核实是否为施工影响或井盖破损。'
-  if (alert.alertType.includes('水位')) return '水位突增至58cm,查看过去2小时降雨量数据无雨情,可能为管道堵塞,建议立即排水检修,排除误报。'
-  if (alert.alertType.includes('震动')) return '震动峰值112mg,周边无大型车辆记录,可能由于井盖松动或人为敲击,建议巡检。'
-  return '结合历史报警记录,该测点一周内无相似报警,当前数据超出阈值明显,真报警可能性较高,建议优先处置。'
-})
+  if (battery !== null && battery <= 20) {
+    return {
+      hasAlert: true,
+      level: 'warning',
+      levelText: '一般报警',
+      alertType: '低电量预警',
+      alertParam: '电池电量',
+      realData: `${battery}%`,
+      threshold: '20%'
+    }
+  }
 
-const similarAlerts = computed(() => {
-  if (!selectedAlert.value) return []
-  return alerts.value.filter(a => a.alertType === selectedAlert.value.alertType && a.id !== selectedAlert.value.id && a.status === 'resolved').slice(0, 3).map(a => ({
-    time: a.time, alertType: a.alertType, realData: a.realData, result: a.result || '真报警'
-  }))
-})
+  if (signal !== null && signal <= 20) {
+    return {
+      hasAlert: true,
+      level: 'warning',
+      levelText: '一般报警',
+      alertType: '弱信号预警',
+      alertParam: '信号强度',
+      realData: `${signal}%`,
+      threshold: '20%'
+    }
+  }
+
+  return {
+    hasAlert: false,
+    level: '',
+    levelText: '',
+    alertType: '',
+    alertParam: '',
+    realData: '',
+    threshold: '—'
+  }
+}
 
-const startProcess = () => {
-  if (selectedAlert.value.status === 'pending') {
-    selectedAlert.value.status = 'processing'
-    selectedAlert.value.logs.push({ id: Date.now(), time: new Date().toLocaleString(), content: '值班人员开始处理报警', type: 'warning' })
-    ElMessage.info('已开始处理该报警事件')
+function normalizeAlert(raw, baseAlert = null) {
+  const alertInfo = buildAlertInfo(raw)
+  if (!alertInfo.hasAlert) return null
+  return {
+    id: raw?.equipmentId || raw?.equipmentCode,
+    equipmentId: raw?.equipmentId || '',
+    deviceId: raw?.equipmentCode || raw?.equipmentId || '—',
+    deviceName: raw?.equipmentName || raw?.equipmentCode || '未命名设备',
+    deviceType: raw?.equipmentTypeName || '井盖监测设备',
+    location: raw?.equipmentLocation || '暂无位置信息',
+    lng: raw?.longitude || baseAlert?.lng || '',
+    lat: raw?.latitude || baseAlert?.lat || '',
+    alertType: alertInfo.alertType,
+    alertParam: alertInfo.alertParam,
+    realData: alertInfo.realData,
+    threshold: alertInfo.threshold,
+    exceedValue: alertInfo.realData || '报警',
+    level: alertInfo.level,
+    levelText: alertInfo.levelText,
+    time: formatDateTime(raw?.equipmentStatus?.statusUpdateTime || raw?.updateTime || raw?.installTime),
+    status: raw?.equipmentStatus?.alarmStatus === 1 ? 'pending' : 'resolved',
+    statusText: raw?.equipmentStatus?.alarmStatus === 1 ? '待处理' : '已恢复',
+    raw
   }
 }
 
-const resolveAsTrue = () => {
-  if (selectedAlert.value.status === 'resolved') return
-  selectedAlert.value.status = 'resolved'
-  selectedAlert.value.result = '真报警'
-  selectedAlert.value.logs.push({ id: Date.now(), time: new Date().toLocaleString(), content: '经研判确认为真报警,已派发维修工单', type: 'success' })
-  ElMessage.success('已确认真报警,处置流程已启动')
+async function loadAlerts() {
+  listLoading.value = true
+  try {
+    const res = await getManholeAlarmDeviceList({
+      alarmStatus: 1,
+      isQueryManholeData: true
+    })
+    const list = Array.isArray(res?.data) ? res.data : []
+    alerts.value = list
+      .map(item => normalizeAlert(item))
+      .filter(Boolean)
+    if (!alerts.value.length) {
+      selectedAlert.value = null
+      return
+    }
+    const currentId = selectedAlert.value?.id
+    selectedAlert.value = alerts.value.find(item => item.id === currentId) || alerts.value[0]
+    if (selectedAlert.value?.equipmentId) {
+      await loadAlertDetail(selectedAlert.value)
+    }
+  } catch (error) {
+    alerts.value = []
+    selectedAlert.value = null
+    ElMessage.error('报警设备加载失败')
+  } finally {
+    listLoading.value = false
+  }
 }
 
-const resolveAsFalse = () => {
-  if (selectedAlert.value.status === 'resolved') return
-  selectedAlert.value.status = 'resolved'
-  selectedAlert.value.result = '误报警'
-  selectedAlert.value.logs.push({ id: Date.now(), time: new Date().toLocaleString(), content: '经现场核实,该报警为误报(环境干扰/传感器波动),已排除', type: 'info' })
-  ElMessage.info('已标记为误报警,事件关闭')
+async function loadAlertDetail(alert) {
+  if (!alert?.equipmentId) return
+  detailLoading.value = true
+  try {
+    const res = await getManholeAlarmDeviceDetail(alert.equipmentId)
+    if (res?.data) {
+      const merged = normalizeAlert(res.data, alert)
+      if (merged) {
+        const idx = alerts.value.findIndex(item => item.id === merged.id)
+        if (idx > -1) {
+          alerts.value[idx] = merged
+        }
+        selectedAlert.value = merged
+      }
+    }
+  } catch (error) {
+    ElMessage.error('报警详情加载失败')
+  } finally {
+    detailLoading.value = false
+  }
 }
 
-const viewOnMap = () => { mapDialogVisible.value = true }
-const addNote = () => {
-  const note = prompt('请输入研判备注信息')
-  if (note && selectedAlert.value) {
-    selectedAlert.value.logs.push({ id: Date.now(), time: new Date().toLocaleString(), content: `备注: ${note}`, type: 'info' })
-    ElMessage.success('备注已添加')
+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.deviceId || ''}`.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 filterAlerts = (type) => {
+  filterType.value = type
+}
+
+const selectAlert = async (alert) => {
+  selectedAlert.value = alert
+  await loadAlertDetail(alert)
 }
+
+const viewOnMap = () => {
+  mapDialogVisible.value = true
+}
+
+onMounted(async () => {
+  await loadAlerts()
+})
 </script>
 
 <style scoped>
@@ -433,9 +445,6 @@ const addNote = () => {
 }
 .real-time-data { background: #fff9f0; padding: 8px; border-radius: 8px; }
 .warning-value { color: #f56c6c; font-size: 18px; margin: 0 4px; }
-.chart-container { height: 220px; }
-.analysis-tips { margin-top: 12px; }
 .action-buttons { display: flex; gap: 8px; }
 .empty-detail { flex: 1; display: flex; align-items: center; justify-content: center; background: white; border-radius: 20px; }
-.history-ref { margin-top: 8px; }
-</style>
+</style>

+ 606 - 183
src/views/subSystem/manholeCover/alarm/alarmSanalysis.vue

@@ -28,9 +28,28 @@
       <!-- 左侧图层控制面板 -->
       <div class="layer-control">
         <div class="layer-header">
-          <el-icon>x</el-icon> 态势图层
+          <el-icon><Monitor /></el-icon> 态势图层
+        </div>
+        <div class="layer-filter-panel">
+          <el-input
+            v-model="queryParams.keyword"
+            clearable
+            :prefix-icon="Search"
+            placeholder="设备编号/名称/位置模糊查询"
+            @keyup.enter="handleSearch"
+          />
+          <div class="filter-actions">
+            <el-button type="primary" size="small" @click="handleSearch">查询</el-button>
+            <el-button size="small" :icon="RefreshRight" @click="handleReset">重置</el-button>
+          </div>
+          <div class="filter-summary">
+            <el-tag size="small" effect="plain">搜索结果 {{ totalDevices }} 台</el-tag>
+            <el-tag size="small" type="success" effect="plain">当前显示 {{ displayDevices.length }} 台</el-tag>
+            <el-tag v-if="activeQuickFilter" size="small" type="warning" effect="plain">态势筛选: {{ quickFilterLabel }}</el-tag>
+          </div>
         </div>
         <el-tree
+            ref="layerTreeRef"
             :data="layerTree"
             show-checkbox
             node-key="id"
@@ -57,6 +76,7 @@
         <div class="time-selector">
           <el-date-picker v-model="timeRange" type="datetimerange" range-separator="至" size="small" :shortcuts="shortcuts" start-placeholder="开始" end-placeholder="结束" />
         </div>
+        <div class="panel-tip">已选图层会实时过滤地图点位,支持结合关键字一起查询。</div>
       </div>
 
       <!-- 右侧GIS地图核心区 -->
@@ -68,23 +88,23 @@
             <el-button size="small" :icon="FullScreen" @click="resetView">重置视图</el-button>
           </el-button-group>
           <div class="current-stat">
-            <span><el-icon><Position /></el-icon> 监测点总数: {{ totalDevices }}</span>
+            <span><el-icon><Position /></el-icon> 设备总数: {{ totalDevices }}</span>
+            <span><el-icon><Monitor /></el-icon> 当前显示: {{ displayDevices.length }}</span>
             <span class="alert-summary">报警中: {{ totalActiveAlerts }}</span>
           </div>
         </div>
-        <div class="map-container" ref="mapContainer">
-          <canvas id="alertMapCanvas" ref="canvasRef" @click="onMapClick" @mousemove="onCanvasMouseMove"></canvas>
-          <!-- 悬浮信息卡 -->
-          <div v-if="hoverDevice" class="hover-card" :style="{ top: hoverY + 'px', left: hoverX + 'px' }">
-            <div class="hover-title">{{ hoverDevice.name }}</div>
-            <div>状态: {{ hoverDevice.statusText }}</div>
-            <div>报警: {{ hoverDevice.hasAlert ? hoverDevice.alertType : '无' }}</div>
-            <div>位置: {{ hoverDevice.locationShort }}</div>
+        <div class="map-container" ref="mapContainer" v-loading="listLoading">
+          <div ref="baiduMapRef" class="baidu-map"></div>
+          <div v-if="!listLoading && !displayDevices.length" class="empty-state">
+            当前筛选条件下暂无设备
+          </div>
+          <div v-if="mapError" class="map-error">
+            {{ mapError }}
           </div>
         </div>
         <!-- 右下角报警聚合弹窗/详情 -->
         <el-drawer v-model="drawerVisible" :title="selectedDevice?.name + ' - 监测报警详情'" direction="rtl" size="420px">
-          <div v-if="selectedDevice">
+          <div v-if="selectedDevice" v-loading="detailLoading">
             <el-descriptions :column="1" border>
               <el-descriptions-item label="设备编号">{{ selectedDevice.id }}</el-descriptions-item>
               <el-descriptions-item label="地理位置">{{ selectedDevice.address }}</el-descriptions-item>
@@ -118,62 +138,105 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
-import { MapLocation, ZoomIn, ZoomOut, FullScreen, Position, WarningFilled, Location, Monitor, Opportunity } from '@element-plus/icons-vue'
-import { ElMessage } from 'element-plus'
+import {computed, nextTick, onMounted, onUnmounted, ref, watch} from 'vue'
+import {
+  Connection,
+  FullScreen,
+  InfoFilled,
+  Monitor,
+  Position,
+  RefreshRight,
+  Search,
+  Setting,
+  WarningFilled,
+  ZoomIn,
+  ZoomOut
+} from '@element-plus/icons-vue'
+import {ElMessage} from 'element-plus'
+import {getManholeAlarmDeviceDetail, getManholeAlarmDeviceList} from '@/api/pipeNetwork/basic'
 
-// 图层树结构(分图层汇聚展示)
 const layerTree = ref([
   {
-    id: 'layer-status',
-    label: '井盖本体状态图层',
-    icon: 'Monitor',
-    alertCount: 3,
+    id: 'current-status',
+    label: '运维状态筛选',
+    icon: Setting,
+    alertCount: 0,
     children: [
-      { id: 'tilt', label: '位移/倾斜监测', icon: 'Position', alertCount: 2 },
-      { id: 'open', label: '开盖状态监测', icon: 'Lock', alertCount: 1 },
-      { id: 'vibration', label: '震动异常监测', icon: 'Opportunity', alertCount: 0 }
+      { id: 'current-1', label: '在用设备', icon: Monitor, alertCount: 0 },
+      { id: 'current-3', label: '维修设备', icon: Setting, alertCount: 0 }
     ]
   },
   {
-    id: 'layer-alert',
-    label: '实时报警图层',
-    icon: 'WarningFilled',
-    alertCount: 3,
+    id: 'alarm-status',
+    label: '报警状态筛选',
+    icon: WarningFilled,
+    alertCount: 0,
     children: [
-      { id: 'critical-alert', label: '严重报警点', icon: 'WarningFilled', alertCount: 2 },
-      { id: 'warning-alert', label: '一般报警点', icon: 'InfoFilled', alertCount: 1 }
+      { id: 'alarm-1', label: '报警设备', icon: WarningFilled, alertCount: 0 },
+      { id: 'alarm-0', label: '正常设备', icon: InfoFilled, alertCount: 0 }
     ]
   },
   {
-    id: 'layer-device',
-    label: '设备运维图层',
-    icon: 'Setting',
-    alertCount: 1,
+    id: 'online-status',
+    label: '在线状态筛选',
+    icon: Connection,
+    alertCount: 0,
     children: [
-      { id: 'battery-low', label: '低电量设备', icon: 'Battery', alertCount: 1 },
-      { id: 'offline', label: '离线设备', icon: 'Connection', alertCount: 1 }
+      { id: 'online-1', label: '在线设备', icon: Connection, alertCount: 0 },
+      { id: 'online-0', label: '离线设备', icon: Connection, alertCount: 0 }
     ]
   }
 ])
-const defaultCheckedLayers = ['layer-status', 'layer-alert', 'tilt', 'open', 'critical-alert', 'warning-alert']
+const defaultCheckedLayers = ['current-1', 'current-3', 'alarm-1', 'alarm-0', 'online-1', 'online-0']
 const visibleLayers = ref(new Set(defaultCheckedLayers))
+const layerTreeRef = ref(null)
+const queryParams = ref({
+  keyword: ''
+})
+const mockManholes = ref([])
+const listLoading = ref(false)
+const detailLoading = ref(false)
+const activeQuickFilter = ref('')
+const baiduMapRef = ref(null)
+const mapError = ref('')
 
-// 模拟窨井盖设备数据 (含报警态势)
-const mockManholes = ref([
-  { id: 'MN-001', name: '人民路1号', lng: 121.487, lat: 31.249, address: '人民路与解放路口', status: 'critical', statusText: '严重报警', hasAlert: true, level: 'critical', alertType: '倾斜超标', realTimeValue: '14.2°', threshold: '8°', lastComm: '2025-04-02 14:20', locationShort: '人民路口', battery: 67,
-    alertLogs: [{ time: '2025-04-02 14:18', content: '倾斜角14.2°超过阈值8°' }] },
-  { id: 'MN-002', name: '滨江路防爆井', lng: 121.502, lat: 31.235, address: '滨江路化工园区', status: 'critical', statusText: '严重报警', hasAlert: true, level: 'critical', alertType: '水位超标', realTimeValue: '62cm', threshold: '40cm', lastComm: '2025-04-02 14:05', locationShort: '滨江园区', battery: 34,
-    alertLogs: [{ time: '2025-04-02 13:58', content: '水位62cm超阈值' }] },
-  { id: 'MN-003', name: '古城街井盖', lng: 121.478, lat: 31.258, address: '古城街北段', status: 'warning', statusText: '一般报警', hasAlert: true, level: 'warning', alertType: '异常震动', realTimeValue: '112mg', threshold: '80mg', lastComm: '2025-04-02 13:30', locationShort: '古城街', battery: 55,
-    alertLogs: [{ time: '2025-04-02 13:25', content: '震动112mg异常' }] },
-  { id: 'MN-004', name: '高新区智能井', lng: 121.512, lat: 31.242, address: '高新大道云计算中心', status: 'normal', statusText: '正常', hasAlert: false, lastComm: '2025-04-02 14:15', locationShort: '高新区', battery: 92 },
-  { id: 'MN-005', name: '南港路井盖', lng: 121.495, lat: 31.228, address: '南港路旧车站', status: 'warning', statusText: '一般报警', hasAlert: true, level: 'warning', alertType: '井盖开启异常', realTimeValue: '开启', threshold: '关闭', lastComm: '2025-04-02 12:50', locationShort: '南港路', battery: 41,
-    alertLogs: [{ time: '2025-04-02 12:48', content: '井盖非正常开启' }] },
-  { id: 'MN-006', name: '石化区防爆井', lng: 121.524, lat: 31.239, address: '石化大道', status: 'offline', statusText: '离线', hasAlert: false, lastComm: '2025-04-01 20:00', locationShort: '石化区', battery: 12 }
-])
+const BAIDU_MAP_AK = import.meta.env.VITE_BAIDU_MAP_AK || ''
+const DEFAULT_MAP_CENTER = { lng: 110.393, lat: 28.452 }
+
+let mapInstance = null
+let mapOverlays = []
+let baiduMapScriptPromise = null
+
+const quickFilterLabelMap = {
+  critical: '严重报警',
+  warning: '一般报警',
+  normal: '正常井盖',
+  offline: '离线设备'
+}
+
+const quickFilterLabel = computed(() => quickFilterLabelMap[activeQuickFilter.value] || '')
+
+function isBatteryLow(device) {
+  const battery = toNumber(device.battery)
+  return battery !== null && battery <= 20
+}
+
+function matchesQuickFilter(device) {
+  if (!activeQuickFilter.value) return true
+  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
+}
+
+const displayDevices = computed(() => {
+  return mockManholes.value.filter(matchesQuickFilter)
+})
 
-// 统计
 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)
@@ -181,143 +244,437 @@ const offlineCount = computed(() => mockManholes.value.filter(m => m.status ===
 const totalDevices = computed(() => mockManholes.value.length)
 const totalActiveAlerts = computed(() => criticalCount.value + warningCount.value)
 
-// GIS Canvas 绘图部分
-const canvasRef = ref(null)
 const mapContainer = ref(null)
-let ctx = null
-let mapWidth = 0, mapHeight = 0
-let mapZoom = 1
-let offsetX = 0, offsetY = 0
-const bounds = { minLng: 121.45, maxLng: 121.55, minLat: 31.22, maxLat: 31.28 }
-
-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 cx = mapWidth / 2, cy = mapHeight / 2
-  return { x: (px - cx) * mapZoom + cx + offsetX, y: (py - cy) * mapZoom + cy + offsetY }
-}
-
-function drawBaseMap() {
-  if (!ctx) return
-  ctx.fillStyle = '#eaf2e6'
-  ctx.fillRect(0, 0, mapWidth, mapHeight)
-  ctx.strokeStyle = '#c8ddc4'
-  ctx.lineWidth = 1
-  for (let i = 0; i < 12; i++) {
-    ctx.beginPath()
-    ctx.moveTo((i / 12) * mapWidth, 0)
-    ctx.lineTo((i / 12) * mapWidth, mapHeight)
-    ctx.stroke()
-    ctx.beginPath()
-    ctx.moveTo(0, (i / 12) * mapHeight)
-    ctx.lineTo(mapWidth, (i / 12) * mapHeight)
-    ctx.stroke()
+const bounds = ref({ minLng: 121.45, maxLng: 121.55, minLat: 31.22, maxLat: 31.28 })
+
+function toNumber(value) {
+  const num = Number(value)
+  return Number.isFinite(num) ? num : null
+}
+
+function formatDateTime(value) {
+  return value || '—'
+}
+
+function isValidCoordinate(lng, lat) {
+  return lng !== null && lat !== null && lng >= 70 && lng <= 140 && lat >= 3 && lat <= 55
+}
+
+function getThresholdTextByCode(raw, warningCode) {
+  const list = Array.isArray(raw?.warningThresholdList) ? raw.warningThresholdList : []
+  const target = list.find(item => item.warningCode === warningCode)
+  if (!target) return '—'
+  const { minValue, maxValue } = target
+  if (minValue !== null && minValue !== undefined && maxValue !== null && maxValue !== undefined) {
+    return `${minValue} ~ ${maxValue}`
+  }
+  if (minValue !== null && minValue !== undefined) return `<= ${minValue}`
+  if (maxValue !== null && maxValue !== undefined) return `>= ${maxValue}`
+  return '—'
+}
+
+function buildAlertInfo(raw) {
+  const status = raw?.equipmentStatus || {}
+  const manholeData = raw?.manholeData || {}
+  const battery = toNumber(manholeData.batteryLevel)
+  const signal = toNumber(manholeData.signalStrength)
+  const tiltAngle = manholeData?.tiltAngle
+
+  if (Number(status.onlineStatus) !== 1) {
+    return { status: 'offline', statusText: '离线', hasAlert: false, level: '', alertType: '', realTimeValue: '—', threshold: '—' }
+  }
+
+  if (`${manholeData.waterLevelAlarmStatus}` === '1') {
+    return {
+      status: 'critical',
+      statusText: '严重报警',
+      hasAlert: true,
+      level: 'critical',
+      alertType: '水位报警',
+      realTimeValue: '报警',
+      threshold: getThresholdTextByCode(raw, 'WARN-WATER-LEVEL')
+    }
   }
-  ctx.fillStyle = '#a8d0a6'
-  ctx.font = '12px "Microsoft YaHei"'
-  ctx.fillText('人民路', 180, 110)
-  ctx.fillText('滨江路', 380, 280)
-  ctx.fillText('高新区', 580, 170)
-}
-
-function drawManholes() {
-  if (!ctx) return
-  mockManholes.value.forEach(device => {
-    const { x, y } = worldToCanvas(device.lng, device.lat)
-    if (x < -40 || x > mapWidth + 40 || y < -40 || y > mapHeight + 40) return
-    // 根据状态确定图标样式
-    let color = '#67C23A'
-    let size = 14
-    let isAlertBlink = false
-    if (device.status === 'critical') { color = '#F56C6C'; size = 18; isAlertBlink = true }
-    else if (device.status === 'warning') { color = '#E6A23C'; size = 16 }
-    else if (device.status === 'offline') { color = '#909399'; size = 12 }
-    // 闪烁效果: 简单模拟根据时间正弦变化,但在重绘中直接绘制不闪烁,但可增加外圈
-    ctx.beginPath()
-    ctx.arc(x, y, size, 0, 2 * Math.PI)
-    ctx.fillStyle = color
-    ctx.fill()
-    ctx.strokeStyle = '#fff'
-    ctx.lineWidth = 2
-    ctx.stroke()
-    if (isAlertBlink) {
-      ctx.beginPath()
-      ctx.arc(x, y, size + 4, 0, 2 * Math.PI)
-      ctx.fillStyle = 'rgba(245,108,108,0.3)'
-      ctx.fill()
+
+  if (`${manholeData.waterInfiltrationAlarmStatus}` === '1') {
+    return {
+      status: 'critical',
+      statusText: '严重报警',
+      hasAlert: true,
+      level: 'critical',
+      alertType: '水浸报警',
+      realTimeValue: '报警',
+      threshold: getThresholdTextByCode(raw, 'WARN-WATER-INFILTRATION')
     }
-    ctx.fillStyle = '#fff'
-    ctx.font = 'bold 12px sans-serif'
-    ctx.fillText(device.name.slice(0, 4), x-10, y-8)
-    // 图层过滤额外绘制报警标识
-    if (visibleLayers.value.has('critical-alert') && device.status === 'critical') {
-      ctx.fillStyle = '#ff4d4f'
-      ctx.beginPath()
-      ctx.moveTo(x+12, y-12)
-      ctx.lineTo(x+18, y-5)
-      ctx.fill()
+  }
+
+  if (Number(status.alarmStatus) === 1) {
+    return {
+      status: 'critical',
+      statusText: '严重报警',
+      hasAlert: true,
+      level: 'critical',
+      alertType: tiltAngle ? '倾斜报警' : '设备报警',
+      realTimeValue: tiltAngle ? `${tiltAngle}°` : '报警',
+      threshold: tiltAngle ? getThresholdTextByCode(raw, 'WARN-TILT') : getThresholdTextByCode(raw, 'WARN-DEVICE-ALARM')
     }
-    if (visibleLayers.value.has('tilt') && device.alertType?.includes('倾斜')) {
-      ctx.fillStyle = '#f56c6c'
-      ctx.fillRect(x+8, y-14, 6, 6)
+  }
+
+  if ((battery !== null && battery <= 20) || (signal !== null && signal <= 20)) {
+    return {
+      status: 'warning',
+      statusText: '一般报警',
+      hasAlert: true,
+      level: 'warning',
+      alertType: battery !== null && battery <= 20 ? '低电量预警' : '弱信号预警',
+      realTimeValue: battery !== null && battery <= 20 ? `${battery}%` : `${signal}%`,
+      threshold: '20'
     }
+  }
+
+  return { status: 'normal', statusText: '正常', hasAlert: false, level: '', alertType: '', realTimeValue: '—', threshold: '—' }
+}
+
+function buildAlertLogs(raw, alertInfo) {
+  if (!alertInfo?.hasAlert) return []
+  const timestamp = raw?.equipmentStatus?.statusUpdateTime || raw?.updateTime || raw?.installTime || ''
+  return [{
+    time: formatDateTime(timestamp),
+    content: `${alertInfo.alertType}${alertInfo.realTimeValue && alertInfo.realTimeValue !== '—' ? `:${alertInfo.realTimeValue}` : ''}${alertInfo.threshold && alertInfo.threshold !== '—' ? `,阈值 ${alertInfo.threshold}` : ''}`
+  }]
+}
+
+function normalizeDevice(raw, _index = 0, baseDevice = {}) {
+  const rawLng = toNumber(raw?.longitude)
+  const rawLat = toNumber(raw?.latitude)
+  const baseLng = toNumber(baseDevice.lng)
+  const baseLat = toNumber(baseDevice.lat)
+  const useRawCoordinate = isValidCoordinate(rawLng, rawLat)
+  const useBaseCoordinate = isValidCoordinate(baseLng, baseLat)
+  const lng = useRawCoordinate ? rawLng : (useBaseCoordinate ? baseLng : null)
+  const lat = useRawCoordinate ? rawLat : (useBaseCoordinate ? baseLat : null)
+  const alertInfo = buildAlertInfo(raw)
+  const address = raw?.equipmentLocation || baseDevice.address || '暂无位置信息'
+  return {
+    equipmentId: raw?.equipmentId || baseDevice.equipmentId || '',
+    id: raw?.equipmentCode || baseDevice.id || raw?.equipmentId || '',
+    name: raw?.equipmentName || baseDevice.name || raw?.equipmentCode || '未命名设备',
+    lng,
+    lat,
+    address,
+    status: alertInfo.status,
+    statusText: alertInfo.statusText,
+    hasAlert: alertInfo.hasAlert,
+    level: alertInfo.level,
+    alertType: alertInfo.alertType,
+    realTimeValue: alertInfo.realTimeValue,
+    threshold: alertInfo.threshold,
+    lastComm: formatDateTime(raw?.equipmentStatus?.statusUpdateTime || raw?.updateTime || raw?.installTime),
+    locationShort: address.length > 8 ? `${address.slice(0, 8)}...` : address,
+    battery: raw?.manholeData?.batteryLevel || '—',
+    alertLogs: buildAlertLogs(raw, alertInfo),
+    equipmentTypeName: raw?.equipmentTypeName || '—',
+    equipmentModel: raw?.equipmentModel || '—',
+    manufacturer: raw?.manufacturer || '—',
+    installTime: formatDateTime(raw?.installTime),
+    maintainer: raw?.maintainer || '—',
+    raw
+  }
+}
+
+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
+}
+
+function refreshBounds() {
+  const validList = displayDevices.value.filter(item => Number.isFinite(item.lng) && Number.isFinite(item.lat))
+  if (!validList.length) {
+    bounds.value = { minLng: 110.35, maxLng: 110.43, minLat: 28.42, maxLat: 28.5 }
+    return
+  }
+  const lngList = validList.map(item => item.lng)
+  const latList = validList.map(item => item.lat)
+  const minLng = Math.min(...lngList)
+  const maxLng = Math.max(...lngList)
+  const minLat = Math.min(...latList)
+  const maxLat = Math.max(...latList)
+  bounds.value = {
+    minLng: minLng - 0.01,
+    maxLng: maxLng + 0.01,
+    minLat: minLat - 0.01,
+    maxLat: maxLat + 0.01
+  }
+}
+
+function getStatusColor(status) {
+  if (status === 'critical') return '#f56c6c'
+  if (status === 'warning') return '#e6a23c'
+  if (status === 'offline') return '#909399'
+  return '#67c23a'
+}
+
+function getMarkerHtml(device, isActive = false) {
+  const color = getStatusColor(device.status)
+  const pulse = device.status === 'critical'
+    ? 'box-shadow: 0 0 0 8px rgba(245,108,108,0.18);'
+    : ''
+  const activeRing = isActive
+    ? 'border: 3px solid #ffffff; transform: scale(1.08);'
+    : 'border: 2px solid #ffffff;'
+  return `
+    <div style="display:flex;flex-direction:column;align-items:center;transform:translateY(-6px);cursor:pointer;">
+      <div style="max-width:160px;padding:4px 8px;border-radius:999px;background:rgba(20,33,61,0.82);color:#fff;font-size:12px;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:6px;">
+        ${device.name}
+      </div>
+      <div style="width:18px;height:18px;border-radius:50%;background:${color};${activeRing}${pulse}"></div>
+    </div>
+  `
+}
+
+function clearMapOverlays() {
+  if (!mapInstance) return
+  mapOverlays.forEach((overlay) => {
+    try {
+      mapInstance.removeOverlay(overlay)
+    } catch (_) {}
   })
+  mapOverlays = []
+}
+
+function fitMapViewport() {
+  if (!mapInstance || !window.BMapGL) return
+  if (!displayDevices.value.length) {
+    mapInstance.centerAndZoom(new window.BMapGL.Point(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 14)
+    return
+  }
+  const points = displayDevices.value
+    .filter(device => isValidCoordinate(device.lng, device.lat))
+    .map(device => new window.BMapGL.Point(device.lng, device.lat))
+  if (!points.length) {
+    mapInstance.centerAndZoom(new window.BMapGL.Point(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 14)
+    return
+  }
+  mapInstance.setViewport(points)
+  if (mapInstance.getZoom() > 16) {
+    mapInstance.setZoom(16)
+  }
+  if (mapInstance.getZoom() < 12) {
+    mapInstance.setZoom(12)
+  }
+}
+
+function renderMapOverlays() {
+  if (!mapInstance || !window.BMapGL) return
+  clearMapOverlays()
+  displayDevices.value
+    .filter(device => isValidCoordinate(device.lng, device.lat))
+    .forEach((device) => {
+    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), {
+      position: point,
+      offset: new window.BMapGL.Size(-30, -52)
+    })
+    label.setStyle({
+      border: 'none',
+      background: 'transparent',
+      padding: '0'
+    })
+    label.addEventListener('click', async (event) => {
+      event?.domEvent?.stopPropagation?.()
+      await openDeviceDetail(device)
+    })
+    mapInstance.addOverlay(label)
+    mapOverlays.push(label)
+    })
+  fitMapViewport()
 }
 
 function renderMap() {
-  if (!canvasRef.value || !mapContainer.value) return
-  const canvas = canvasRef.value
-  mapWidth = mapContainer.value.clientWidth
-  mapHeight = mapContainer.value.clientHeight
-  canvas.width = mapWidth
-  canvas.height = mapHeight
-  ctx = canvas.getContext('2d')
-  drawBaseMap()
-  drawManholes()
+  if (!mapInstance || !window.BMapGL) return
+  renderMapOverlays()
 }
 
-function zoomIn() { mapZoom = Math.min(mapZoom + 0.15, 2.5); renderMap() }
-function zoomOut() { mapZoom = Math.max(mapZoom - 0.15, 0.6); renderMap() }
-function resetView() { mapZoom = 1; offsetX = 0; offsetY = 0; renderMap() }
+function ensureBaiduMap() {
+  if (window.BMapGL) {
+    return Promise.resolve(window.BMapGL)
+  }
+  if (baiduMapScriptPromise) {
+    return baiduMapScriptPromise
+  }
+  if (!BAIDU_MAP_AK) {
+    return Promise.reject(new Error('未配置百度地图 AK,请在 .env.development 中配置 VITE_BAIDU_MAP_AK'))
+  }
+
+  baiduMapScriptPromise = new Promise((resolve, reject) => {
+    const existing = document.querySelector('script[data-baidu-map="webgl"]')
+    if (existing) {
+      let attempts = 0
+      const timer = window.setInterval(() => {
+        attempts++
+        if (window.BMapGL) {
+          window.clearInterval(timer)
+          resolve(window.BMapGL)
+        } else if (attempts >= 50) {
+          window.clearInterval(timer)
+          reject(new Error('百度地图脚本加载超时'))
+        }
+      }, 200)
+      return
+    }
+
+    const script = document.createElement('script')
+    script.src = `https://api.map.baidu.com/api?type=webgl&v=1.0&ak=${BAIDU_MAP_AK}`
+    script.async = true
+    script.defer = true
+    script.setAttribute('data-baidu-map', 'webgl')
+    script.onload = () => {
+      let attempts = 0
+      const timer = window.setInterval(() => {
+        attempts++
+        if (window.BMapGL) {
+          window.clearInterval(timer)
+          resolve(window.BMapGL)
+        } else if (attempts >= 50) {
+          window.clearInterval(timer)
+          reject(new Error('百度地图脚本初始化超时'))
+        }
+      }, 200)
+    }
+    script.onerror = () => reject(new Error('百度地图脚本加载失败'))
+    document.head.appendChild(script)
+  })
+
+  return baiduMapScriptPromise
+}
 
-const onLayerChange = () => { renderMap() }
-const focusLayer = (type) => { /* 可缩放至对应类型设备,简化处理 */ ElMessage.info(`聚焦${type}类设备`) }
+async function initMap() {
+  if (!baiduMapRef.value) return
+  try {
+    await ensureBaiduMap()
+    if (!mapInstance) {
+      mapInstance = new window.BMapGL.Map(baiduMapRef.value, { enableMapClick: true })
+      mapInstance.enableScrollWheelZoom(true)
+      mapInstance.setMinZoom(11)
+      mapInstance.setMaxZoom(18)
+      mapInstance.centerAndZoom(new window.BMapGL.Point(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 14)
+      mapInstance.addEventListener('click', () => {
+        drawerVisible.value = false
+      })
+    }
+    mapError.value = ''
+    renderMap()
+  } catch (error) {
+    mapError.value = error?.message || '百度地图加载失败'
+  }
+}
+
+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: 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()
+  } catch (error) {
+    mockManholes.value = []
+    ElMessage.error('井盖设备数据加载失败')
+  } finally {
+    listLoading.value = false
+  }
+}
+
+async function openDeviceDetail(device) {
+  if (!device?.equipmentId) return
+  selectedDevice.value = device
+  drawerVisible.value = true
+  detailLoading.value = true
+  try {
+    const res = await getManholeAlarmDeviceDetail(device.equipmentId)
+    if (res?.data) {
+      const index = mockManholes.value.findIndex(item => item.equipmentId === device.equipmentId)
+      selectedDevice.value = normalizeDevice(res.data, index, device)
+      if (mapInstance && window.BMapGL && isValidCoordinate(selectedDevice.value.lng, selectedDevice.value.lat)) {
+        mapInstance.panTo(new window.BMapGL.Point(selectedDevice.value.lng, selectedDevice.value.lat))
+      }
+      renderMap()
+    }
+  } catch (error) {
+    ElMessage.error('设备详情加载失败')
+  } finally {
+    detailLoading.value = false
+  }
+}
+
+function zoomIn() {
+  if (!mapInstance) return
+  mapInstance.setZoom(Math.min(mapInstance.getZoom() + 1, 18))
+}
+function zoomOut() {
+  if (!mapInstance) return
+  mapInstance.setZoom(Math.max(mapInstance.getZoom() - 1, 11))
+}
+function resetView() {
+  if (!mapInstance || !window.BMapGL) return
+  fitMapViewport()
+}
+
+const onLayerChange = (_data, checkedInfo) => {
+  visibleLayers.value = new Set(checkedInfo.checkedKeys)
+  loadDeviceList()
+}
+const focusLayer = async (type) => {
+  activeQuickFilter.value = activeQuickFilter.value === type ? '' : type
+  const target = displayDevices.value.find(item => item.status === type)
+  if (!target) {
+    if (activeQuickFilter.value) {
+      ElMessage.info('当前筛选下无对应设备')
+    }
+    return
+  }
+  if (!activeQuickFilter.value) {
+    ElMessage.info('当前无对应设备')
+    return
+  }
+  await nextTick()
+  renderMap()
+}
 
-// 交互变量
 const selectedDevice = ref(null)
 const drawerVisible = ref(false)
-const hoverDevice = ref(null)
-const hoverX = ref(0), hoverY = ref(0)
 const timeRange = ref([])
 const shortcuts = [{ text: '最近24小时', value: () => [new Date(Date.now() - 86400000), new Date()] }]
 
-function onMapClick(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
-  mockManholes.value.forEach(device => {
-    const { x, y } = worldToCanvas(device.lng, device.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) { minDist = dist; hit = device }
-  })
-  if (hit) { selectedDevice.value = hit; drawerVisible.value = true }
-}
-
-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
-  mockManholes.value.forEach(device => {
-    const { x, y } = worldToCanvas(device.lng, device.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) { minDist = dist; hit = device }
-  })
-  if (hit) { hoverDevice.value = hit; hoverX.value = e.clientX + 12; hoverY.value = e.clientY - 40 }
-  else hoverDevice.value = null
-}
-
 const getStatusTag = (status) => {
   if (status === 'critical') return 'danger'
   if (status === 'warning') return 'warning'
@@ -327,18 +684,46 @@ const getStatusTag = (status) => {
 const getAlertAdvice = (device) => {
   if (device.alertType?.includes('倾斜')) return '建议立即安排检修人员现场确认井盖固定情况,如损坏需更换。'
   if (device.alertType?.includes('水位')) return '井内水位过高,需紧急排水,检查管道是否堵塞。'
-  if (device.alertType?.includes('震动')) return '持续震动可能为外力破坏,建议巡检周边施工情况。'
+  if (device.alertType?.includes('水浸')) return '建议核查井内积水和周边排水情况,必要时安排现场排查。'
+  if (device.alertType?.includes('低电量')) return '建议尽快安排电池更换,避免设备掉线。'
+  if (device.alertType?.includes('弱信号')) return '建议核查设备天线、网络覆盖和安装环境。'
   return '请根据现场情况迅速核实并处置。'
 }
 const dispatchWorkOrder = () => { ElMessage.success('已派发工单至维护班组') }
 
-onMounted(() => {
+function handleSearch() {
+  activeQuickFilter.value = ''
+  loadDeviceList()
+}
+
+function handleReset() {
+  queryParams.value.keyword = ''
+  activeQuickFilter.value = ''
+  visibleLayers.value = new Set(defaultCheckedLayers)
+  layerTreeRef.value?.setCheckedKeys(defaultCheckedLayers)
+  loadDeviceList()
+}
+
+watch(displayDevices, async () => {
+  refreshBounds()
+  await nextTick()
+  renderMap()
+}, { deep: true })
+
+onMounted(async () => {
+  await nextTick()
+  await initMap()
+  await loadDeviceList()
   nextTick(() => {
     renderMap()
     window.addEventListener('resize', renderMap)
   })
 })
-onUnmounted(() => window.removeEventListener('resize', renderMap))
+onUnmounted(() => {
+  window.removeEventListener('resize', renderMap)
+  clearMapOverlays()
+  mapInstance = null
+})
 </script>
 
 <style scoped>
@@ -379,6 +764,20 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
   overflow-y: auto;
 }
 .layer-header { font-weight: bold; display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
+.layer-filter-panel {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+.filter-actions {
+  display: flex;
+  gap: 8px;
+}
+.filter-summary {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+}
 .layer-tree { flex: 1; }
 .layer-badge { margin-left: auto; }
 .legend-panel { border-top: 1px solid #e9ecef; padding-top: 12px; margin-top: 8px; }
@@ -408,19 +807,43 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
 .map-container {
   flex: 1;
   position: relative;
-  cursor: crosshair;
-}
-canvas { width: 100%; height: 100%; display: block; }
-.hover-card {
-  position: fixed;
-  background: rgba(0,0,0,0.8);
-  color: white;
-  padding: 8px 12px;
+  overflow: hidden;
+}
+.baidu-map {
+  width: 100%;
+  height: 100%;
+}
+.empty-state {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  color: #909399;
+  font-size: 14px;
+  background: rgba(255, 255, 255, 0.92);
+  padding: 10px 18px;
   border-radius: 12px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+  z-index: 2;
+}
+.map-error {
+  position: absolute;
+  top: 16px;
+  right: 16px;
+  max-width: 320px;
+  color: #f56c6c;
+  background: rgba(255, 255, 255, 0.95);
+  border: 1px solid rgba(245, 108, 108, 0.25);
+  border-radius: 12px;
+  padding: 10px 12px;
   font-size: 12px;
-  pointer-events: none;
-  z-index: 200;
-  backdrop-filter: blur(6px);
+  line-height: 1.6;
+  z-index: 3;
 }
 .advice-box { background: #f8f9fa; padding: 12px; border-radius: 12px; }
-</style>
+.panel-tip {
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.5;
+}
+</style>

+ 500 - 351
src/views/subSystem/manholeCover/alarm/index.vue

@@ -1,388 +1,438 @@
-<!--监测阈值管理-->
 <template>
   <div class="threshold-management">
-    <!-- 主内容区域 -->
     <div class="main-content">
-      <!-- 左侧:监测点列表 -->
       <div class="point-list-panel">
         <div class="panel-title">
-          <span>监测点列表</span>
-          <el-input v-model="searchKeyword" placeholder="搜索设备编号/名称" clearable :prefix-icon="Search" size="small" style="width: 180px" />
+          <span>设备列表</span>
+          <el-button :icon="Refresh" text @click="loadDeviceList">刷新</el-button>
         </div>
-        <div class="point-list">
+        <el-input
+          v-model="searchKeyword"
+          placeholder="搜索设备编号/名称"
+          clearable
+          :prefix-icon="Search"
+          size="small"
+        />
+
+        <div v-loading="listLoading" class="point-list">
           <div
-              v-for="point in filteredPoints"
-              :key="point.id"
-              class="point-item"
-              :class="{ active: selectedPoint?.id === point.id }"
-              @click="selectPoint(point)"
+            v-for="point in filteredPoints"
+            :key="point.equipmentId"
+            class="point-item"
+            :class="{ active: selectedPoint?.equipmentId === point.equipmentId }"
+            @click="selectPoint(point)"
           >
             <div class="point-header">
-              <span class="point-name">{{ point.name }}</span>
-              <el-tag :type="getPointStatusType(point)" size="small">{{ point.status }}</el-tag>
+              <span class="point-name">{{ point.equipmentName || point.equipmentCode || '未命名设备' }}</span>
+              <el-tag :type="getAlarmTagType(point)" size="small">
+                {{ getAlarmText(point) }}
+              </el-tag>
             </div>
             <div class="point-location">
-              <el-icon><Location /></el-icon> {{ point.location }}
+              <el-icon><Location /></el-icon>
+              <span>{{ point.equipmentLocation || '暂无位置' }}</span>
+            </div>
+            <div class="point-meta">
+              <span>编号:{{ point.equipmentCode || '-' }}</span>
+              <span>类型:{{ point.equipmentTypeName || '-' }}</span>
+            </div>
+            <div class="point-status-row">
+              <el-tag size="small" effect="plain" :type="getOnlineTagType(point.equipmentStatus?.onlineStatus)">
+                {{ getOnlineText(point.equipmentStatus?.onlineStatus) }}
+              </el-tag>
+              <el-tag size="small" effect="plain" :type="getUseStatusTagType(point.equipmentStatus?.currentStatus)">
+                {{ getUseStatusText(point.equipmentStatus?.currentStatus) }}
+              </el-tag>
             </div>
-            <div class="point-device">{{ point.deviceModel }}</div>
           </div>
+
+          <el-empty
+            v-if="!listLoading && !filteredPoints.length"
+            description="暂无井盖监测设备"
+            :image-size="120"
+          />
         </div>
       </div>
 
-      <!-- 右侧:阈值配置详情区 -->
-      <div class="config-panel" v-if="selectedPoint">
-        <!-- 设备基本信息卡片 -->
+      <div v-if="selectedPoint" v-loading="detailLoading" class="config-panel">
         <div class="info-card">
           <div class="card-title">
-            <span><el-icon><Monitor /></el-icon> 监测设备基本信息</span>
+            <span><el-icon><Monitor /></el-icon> 设备基本信息</span>
           </div>
           <el-descriptions :column="2" border>
-            <el-descriptions-item label="设备编号">{{ selectedPoint.deviceId }}</el-descriptions-item>
-            <el-descriptions-item label="设备类型">{{ selectedPoint.deviceType }}</el-descriptions-item>
-            <el-descriptions-item label="设备型号">{{ selectedPoint.deviceModel }}</el-descriptions-item>
-            <el-descriptions-item label="生产厂商">{{ selectedPoint.manufacturer }}</el-descriptions-item>
-            <el-descriptions-item label="安装日期">{{ selectedPoint.installDate }}</el-descriptions-item>
-            <el-descriptions-item label="通讯协议">{{ selectedPoint.protocol }}</el-descriptions-item>
-            <el-descriptions-item label="监测点ID">{{ selectedPoint.id }}</el-descriptions-item>
-            <el-descriptions-item label="最近校准">{{ selectedPoint.lastCalibration }}</el-descriptions-item>
+            <el-descriptions-item label="设备编号">{{ selectedDetail?.equipmentCode || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="设备名称">{{ selectedDetail?.equipmentName || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="设备类型">{{ selectedDetail?.equipmentTypeName || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="设备型号">{{ selectedDetail?.equipmentModel || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="生产厂商">{{ selectedDetail?.manufacturer || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="安装时间">{{ formatDateTime(selectedDetail?.installTime) }}</el-descriptions-item>
+            <el-descriptions-item label="设备位置">{{ selectedDetail?.equipmentLocation || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="维护人员">{{ selectedDetail?.maintainer || '-' }}</el-descriptions-item>
           </el-descriptions>
         </div>
 
-        <!-- 阈值配置区域 - 多参数标签页 -->
+        <div class="info-card">
+          <div class="card-title">
+            <span><el-icon><Warning /></el-icon> 最新监测状态</span>
+          </div>
+          <div class="status-grid">
+            <div class="status-item">
+              <div class="status-label">在线状态</div>
+              <div class="status-value">{{ getOnlineText(selectedDetail?.equipmentStatus?.onlineStatus) }}</div>
+            </div>
+            <div class="status-item">
+              <div class="status-label">报警状态</div>
+              <div class="status-value">{{ getAlarmText(selectedDetail) }}</div>
+            </div>
+            <div class="status-item">
+              <div class="status-label">倾斜角度</div>
+              <div class="status-value">{{ appendUnit(selectedDetail?.manholeData?.tiltAngle, '°') }}</div>
+            </div>
+            <div class="status-item">
+              <div class="status-label">电池电量</div>
+              <div class="status-value">{{ appendUnit(selectedDetail?.manholeData?.batteryLevel, '%') }}</div>
+            </div>
+            <div class="status-item">
+              <div class="status-label">信号强度</div>
+              <div class="status-value">{{ appendUnit(selectedDetail?.manholeData?.signalStrength, '%') }}</div>
+            </div>
+            <div class="status-item">
+              <div class="status-label">水浸状态</div>
+              <div class="status-value">{{ getBinaryAlarmText(selectedDetail?.manholeData?.waterInfiltrationAlarmStatus) }}</div>
+            </div>
+            <div class="status-item">
+              <div class="status-label">水位状态</div>
+              <div class="status-value">{{ getBinaryAlarmText(selectedDetail?.manholeData?.waterLevelAlarmStatus) }}</div>
+            </div>
+            <div class="status-item">
+              <div class="status-label">激活状态</div>
+              <div class="status-value">{{ selectedDetail?.manholeData?.activationStatus || '-' }}</div>
+            </div>
+          </div>
+        </div>
+
         <div class="threshold-card">
           <div class="card-title">
             <span><el-icon><Setting /></el-icon> 报警阈值设置</span>
             <div class="card-actions">
-              <el-button size="small" @click="resetThresholds">重置</el-button>
-              <el-button size="small" type="primary" @click="saveThresholds">保存设置</el-button>
+              <el-button :icon="Refresh" size="small" @click="resetThresholds">重置</el-button>
+              <el-button :loading="saveLoading" size="small" type="primary" @click="saveThresholds">保存设置</el-button>
             </div>
           </div>
 
-          <el-tabs v-model="activeTab" class="threshold-tabs">
-            <!-- 倾斜阈值 -->
-            <el-tab-pane label="倾斜监测" name="tilt">
-              <div class="threshold-config">
-                <div class="threshold-item">
-                  <div class="item-label">
-                    <span>倾斜角度阈值</span>
-                    <el-tooltip content="当井盖倾斜角度超过此值时触发报警" placement="top">
-                      <el-icon><QuestionFilled /></el-icon>
-                    </el-tooltip>
-                  </div>
-                  <div class="item-control">
-                    <el-input-number v-model="thresholds.tilt.warning" :min="0" :max="45" :step="0.5" size="default" style="width: 120px" />
-                    <span class="unit">度</span>
-                    <span class="level-tag warning">预警值</span>
-                    <el-input-number v-model="thresholds.tilt.critical" :min="0" :max="90" :step="0.5" size="default" style="width: 120px; margin-left: 20px" />
-                    <span class="unit">度</span>
-                    <span class="level-tag critical">告警值</span>
-                  </div>
-                  <div class="history-suggestion">
-                    <el-icon><DataLine /></el-icon>
-                    历史数据分析建议: 基于近30天数据,95%倾斜角在 <strong>{{ suggested.tilt.suggestedWarning }}°</strong> 以内,建议预警值 ≤ {{ suggested.tilt.suggestedWarning }}
-                  </div>
-                </div>
-              </div>
-            </el-tab-pane>
-
-            <!-- 水位/溢水阈值 -->
-            <el-tab-pane label="水位监测" name="water">
-              <div class="threshold-config">
-                <div class="threshold-item">
-                  <div class="item-label">水位深度阈值</div>
-                  <div class="item-control">
-                    <el-input-number v-model="thresholds.water.warning" :min="0" :max="100" :step="1" style="width: 120px" />
-                    <span class="unit">cm</span>
-                    <span class="level-tag warning">预警值</span>
-                    <el-input-number v-model="thresholds.water.critical" :min="0" :max="150" :step="1" style="width: 120px; margin-left: 20px" />
-                    <span class="unit">cm</span>
-                    <span class="level-tag critical">告警值</span>
-                  </div>
-                  <div class="history-suggestion">
-                    <el-icon><DataLine /></el-icon>
-                    历史最高水位: {{ selectedPoint.historyData?.maxWaterLevel || 32 }}cm,建议告警值设置在 {{ suggested.water.suggestedCritical }}cm
-                  </div>
-                </div>
-              </div>
-            </el-tab-pane>
-
-            <!-- 震动阈值 -->
-            <el-tab-pane label="震动监测" name="vibration">
-              <div class="threshold-config">
-                <div class="threshold-item">
-                  <div class="item-label">震动强度阈值</div>
-                  <div class="item-control">
-                    <el-input-number v-model="thresholds.vibration.warning" :min="0" :max="200" :step="5" style="width: 120px" />
-                    <span class="unit">mg</span>
-                    <span class="level-tag warning">预警值</span>
-                    <el-input-number v-model="thresholds.vibration.critical" :min="0" :max="300" :step="5" style="width: 120px; margin-left: 20px" />
-                    <span class="unit">mg</span>
-                    <span class="level-tag critical">告警值</span>
-                  </div>
-                  <div class="history-suggestion">
-                    <el-icon><DataLine /></el-icon>
-                    历史震动峰值: {{ selectedPoint.historyData?.maxVibration || 78 }}mg,建议阈值动态调整
-                  </div>
-                </div>
-              </div>
-            </el-tab-pane>
+          <div class="threshold-tip">
+            当前版本先对接后端已支持的 3 类阈值:倾斜、水位、温度。
+          </div>
 
-            <!-- 气体阈值 -->
-            <el-tab-pane label="有害气体" name="gas">
-              <div class="threshold-config">
-                <div class="threshold-item">
-                  <div class="item-label">H₂S浓度阈值</div>
-                  <div class="item-control">
-                    <el-input-number v-model="thresholds.gas.warning" :min="0" :max="50" :step="1" style="width: 120px" />
-                    <span class="unit">ppm</span>
-                    <span class="level-tag warning">预警值</span>
-                    <el-input-number v-model="thresholds.gas.critical" :min="0" :max="100" :step="1" style="width: 120px; margin-left: 20px" />
-                    <span class="unit">ppm</span>
-                    <span class="level-tag critical">告警值</span>
-                  </div>
-                </div>
-              </div>
-            </el-tab-pane>
+          <el-tabs v-model="activeTab" class="threshold-tabs">
+            <el-tab-pane
+              v-for="item in thresholdConfigList"
+              :key="item.key"
+              :label="item.label"
+              :name="item.key"
+            >
+              <div class="threshold-item">
+                <div class="item-label">{{ item.desc }}</div>
+                <div class="item-control">
+                  <span class="field-label">最小值</span>
+                  <el-input-number
+                    v-model="thresholds[item.key].minValue"
+                    :controls="false"
+                    :precision="item.precision"
+                    :step="item.step"
+                    class="threshold-input"
+                    placeholder="未设置"
+                  />
+                  <span class="unit">{{ item.unit }}</span>
 
-            <!-- 电池/信号阈值 -->
-            <el-tab-pane label="设备状态" name="device">
-              <div class="threshold-config">
-                <div class="threshold-item">
-                  <div class="item-label">电池电压阈值</div>
-                  <div class="item-control">
-                    <el-input-number v-model="thresholds.battery.warning" :min="0" :max="100" :step="5" style="width: 120px" />
-                    <span class="unit">%</span>
-                    <span class="level-tag warning">低电量预警</span>
-                  </div>
+                  <span class="field-label">最大值</span>
+                  <el-input-number
+                    v-model="thresholds[item.key].maxValue"
+                    :controls="false"
+                    :precision="item.precision"
+                    :step="item.step"
+                    class="threshold-input"
+                    placeholder="未设置"
+                  />
+                  <span class="unit">{{ item.unit }}</span>
                 </div>
-                <div class="threshold-item">
-                  <div class="item-label">信号强度阈值</div>
-                  <div class="item-control">
-                    <el-input-number v-model="thresholds.signal.warning" :min="0" :max="100" :step="5" style="width: 120px" />
-                    <span class="unit">%</span>
-                    <span class="level-tag warning">弱信号预警</span>
-                  </div>
+                <el-input
+                  v-model="thresholds[item.key].remark"
+                  maxlength="100"
+                  placeholder="请输入备注"
+                />
+                <div class="saved-hint">
+                  预警编码:{{ item.warningCode }}
+                  <span v-if="thresholds[item.key].id">,当前记录ID:{{ thresholds[item.key].id }}</span>
                 </div>
               </div>
             </el-tab-pane>
           </el-tabs>
         </div>
-
-        <!-- 历史监测数据趋势图 (帮助科学设置阈值) -->
-        <div class="history-chart-card">
-          <div class="card-title">
-            <span><el-icon><TrendCharts /></el-icon> 历史监测数据趋势(近30天)</span>
-            <el-select v-model="historyMetric" size="small" style="width: 120px">
-              <el-option label="倾斜角度" value="tilt" />
-              <el-option label="水位深度" value="water" />
-              <el-option label="震动强度" value="vibration" />
-              <el-option label="气体浓度" value="gas" />
-            </el-select>
-          </div>
-          <div class="chart-container">
-            <v-chart :option="historyChartOption" autoresize />
-          </div>
-
-        </div>
       </div>
 
-      <!-- 未选中状态 -->
       <div v-else class="empty-panel">
-        <el-empty description="请从左侧选择一个监测点查看阈值配置" :image-size="140" />
+        <el-empty description="请从左侧选择设备查看阈值配置" :image-size="140" />
       </div>
     </div>
   </div>
 </template>
 
 <script setup>
-import { ref, computed, watch } from 'vue'
-import { Location, Monitor, Setting, QuestionFilled, DataLine, TrendCharts, Document, Check, Search } from '@element-plus/icons-vue'
-import { use } from 'echarts/core'
-import { CanvasRenderer } from 'echarts/renderers'
-import { LineChart } from 'echarts/charts'
-import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, ToolboxComponent } from 'echarts/components'
+import {computed, onMounted, ref} from 'vue'
+import {ElMessage} from 'element-plus'
+import {Location, Monitor, Refresh, Search, Setting, Warning} from '@element-plus/icons-vue'
+import {
+  getManholeAlarmDeviceDetail,
+  getManholeAlarmDeviceList,
+  saveManholeAlarmThreshold
+} from '@/api/pipeNetwork/basic'
 
-use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, ToolboxComponent])
-
-// 模拟监测点数据
-const monitoringPoints = ref([
+const thresholdConfigList = [
   {
-    id: 'MP-1001',
-    name: '人民路监测点',
-    location: '人民路与解放路口东50m',
-    status: '正常',
-    deviceId: 'MN-1001',
-    deviceType: '智能井盖监测仪',
-    deviceModel: 'WATCHMAN-S2',
-    manufacturer: '华盾科技',
-    installDate: '2024-03-15',
-    protocol: 'LoRaWAN',
-    lastCalibration: '2025-02-10',
-    historyData: {
-      tiltHistory: [1.2, 1.5, 2.1, 1.8, 2.3, 3.2, 2.8, 2.5, 3.0, 2.2, 1.9, 2.4, 3.5, 4.1, 3.8, 3.2, 2.9, 3.3, 4.5, 5.2, 4.8, 4.2, 3.6, 3.9, 4.3, 5.0, 4.6, 4.1, 3.7, 4.2],
-      waterHistory: [5, 6, 7, 8, 10, 12, 15, 18, 20, 22, 25, 28, 30, 32, 35, 38, 36, 34, 33, 35, 37, 40, 42, 39, 36, 34, 32, 30, 28, 26],
-      vibrationHistory: [12, 15, 18, 22, 25, 30, 35, 42, 48, 52, 58, 62, 65, 70, 68, 72, 75, 78, 80, 76, 72, 68, 64, 60, 55, 50, 45, 40, 38, 35],
-      gasHistory: [0.5, 0.8, 1.2, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0, 9.8, 9.5, 9.0, 8.5, 8.0, 7.5, 7.0, 6.5, 6.0],
-      maxWaterLevel: 42,
-      maxVibration: 80
-    }
+    key: 'tilt',
+    label: '倾斜监测',
+    warningType: '倾斜预警',
+    warningCode: 'WARN-TILT',
+    unit: '°',
+    step: 0.1,
+    precision: 1,
+    desc: '当倾斜角度小于等于最小值或大于等于最大值时触发报警'
   },
   {
-    id: 'MP-1002',
-    name: '滨江路监测点',
-    location: '滨江路化工园区南门',
-    status: '告警',
-    deviceId: 'MN-1002',
-    deviceType: '防爆井盖监测仪',
-    deviceModel: 'WATCHMAN-EX',
-    manufacturer: '华盾科技',
-    installDate: '2024-06-20',
-    protocol: 'NB-IoT',
-    lastCalibration: '2025-01-15',
-    historyData: {
-      tiltHistory: [0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 5.2, 6.5, 7.8, 8.2, 9.0, 8.5, 7.5, 6.8, 5.5, 4.8, 4.0, 3.5, 3.0, 2.5, 2.0, 1.8, 1.5, 1.2, 1.0, 0.8],
-      waterHistory: [15, 18, 22, 28, 35, 42, 50, 58, 62, 55, 48, 45, 42, 40, 38, 35, 32, 30, 28, 25, 22, 20, 18, 16, 15, 14, 13, 12, 11, 10],
-      vibrationHistory: [20, 25, 32, 40, 48, 55, 62, 70, 78, 85, 92, 98, 105, 110, 115, 108, 100, 92, 85, 78, 70, 62, 55, 48, 42, 38, 35, 32, 28, 25],
-      gasHistory: [2, 3, 5, 7, 10, 15, 20, 25, 30, 28, 25, 22, 20, 18, 16, 15, 14, 12, 10, 9, 8, 7, 6, 5, 4, 3.5, 3, 2.5, 2, 1.5],
-      maxWaterLevel: 62,
-      maxVibration: 115
-    }
+    key: 'waterLevel',
+    label: '水位监测',
+    warningType: '水位预警',
+    warningCode: 'WARN-WATER-LEVEL',
+    unit: 'cm',
+    step: 0.1,
+    precision: 1,
+    desc: '用于井盖水位报警阈值配置'
   },
   {
-    id: 'MP-1003',
-    name: '高新区监测点',
-    location: '高新大道云计算中心旁',
-    status: '正常',
-    deviceId: 'MN-1004',
-    deviceType: '智能井盖监测仪',
-    deviceModel: 'WATCHMAN-S2',
-    manufacturer: '华盾科技',
-    installDate: '2024-09-10',
-    protocol: 'LoRaWAN',
-    lastCalibration: '2025-03-01',
-    historyData: {
-      tiltHistory: [0.3, 0.5, 0.4, 0.6, 0.8, 0.7, 0.9, 1.0, 0.8, 0.6, 0.5, 0.7, 0.9, 1.1, 1.0, 0.8, 0.6, 0.5, 0.4, 0.6, 0.8, 0.7, 0.5, 0.3, 0.4, 0.6, 0.9, 1.2, 0.9, 0.7],
-      waterHistory: [3, 4, 5, 6, 5, 7, 8, 6, 5, 4, 5, 6, 7, 8, 9, 10, 8, 7, 6, 5, 4, 3, 4, 5, 6, 7, 8, 9, 7, 6],
-      vibrationHistory: [8, 10, 12, 15, 14, 16, 18, 20, 18, 15, 12, 10, 11, 13, 15, 17, 16, 14, 12, 10, 9, 8, 7, 9, 11, 13, 15, 14, 12, 10],
-      gasHistory: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.5, 0.4, 0.3, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.5, 0.4, 0.3, 0.2],
-      maxWaterLevel: 10,
-      maxVibration: 20
-    }
+    key: 'temperature',
+    label: '温度监测',
+    warningType: '温度预警',
+    warningCode: 'WARN-TEMPERATURE',
+    unit: '℃',
+    step: 0.1,
+    precision: 1,
+    desc: '用于设备温度报警阈值配置'
   }
-])
+]
 
 const searchKeyword = ref('')
-const selectedPoint = ref(monitoringPoints.value[0])
 const activeTab = ref('tilt')
-const historyMetric = ref('tilt')
-
-// 当前阈值配置
-const thresholds = ref({
-  tilt: { warning: 5, critical: 10 },
-  water: { warning: 25, critical: 40 },
-  vibration: { warning: 60, critical: 100 },
-  gas: { warning: 10, critical: 20 },
-  battery: { warning: 20 },
-  signal: { warning: 30 }
-})
+const listLoading = ref(false)
+const detailLoading = ref(false)
+const saveLoading = ref(false)
+const deviceList = ref([])
+const selectedPoint = ref(null)
+const selectedDetail = ref(null)
+const thresholds = ref(createEmptyThresholds())
 
-// 智能建议值(基于历史数据)
-const suggested = computed(() => {
-  const hist = selectedPoint.value?.historyData
-  if (!hist) return { tilt: { suggestedWarning: 5 }, water: { suggestedCritical: 40 }, vibration: {} }
-  const tilt95 = hist.tiltHistory ? Math.ceil(hist.tiltHistory.sort((a,b)=>a-b)[Math.floor(hist.tiltHistory.length*0.95)] * 10) / 10 : 5
-  const waterMax = hist.maxWaterLevel || 40
-  return {
-    tilt: { suggestedWarning: Math.min(tilt95 + 1, 12) },
-    water: { suggestedCritical: Math.min(waterMax + 5, 80) },
-    vibration: { suggestedPeak: hist.maxVibration || 80 }
-  }
-})
+function createEmptyThresholds() {
+  return thresholdConfigList.reduce((acc, item) => {
+    acc[item.key] = {
+      id: '',
+      minValue: null,
+      maxValue: null,
+      remark: ''
+    }
+    return acc
+  }, {})
+}
+
+function normalizeThresholdList(list) {
+  const safeList = Array.isArray(list) ? list : []
+  const result = createEmptyThresholds()
+  thresholdConfigList.forEach((item) => {
+    const matched = safeList.find(threshold => threshold.warningCode === item.warningCode)
+    if (matched) {
+      result[item.key] = {
+        id: matched.id || '',
+        minValue: matched.minValue ?? null,
+        maxValue: matched.maxValue ?? null,
+        remark: matched.remark || ''
+      }
+    }
+  })
+  return result
+}
 
 const filteredPoints = computed(() => {
-  if (!searchKeyword.value) return monitoringPoints.value
-  const kw = searchKeyword.value.toLowerCase()
-  return monitoringPoints.value.filter(p =>
-      p.name.toLowerCase().includes(kw) ||
-      p.deviceId.toLowerCase().includes(kw) ||
-      p.location.toLowerCase().includes(kw)
-  )
+  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)
+  })
 })
 
-const getPointStatusType = (point) => point.status === '正常' ? 'success' : 'danger'
+function getOnlineText(status) {
+  return Number(status) === 1 ? '在线' : '离线'
+}
 
-const selectPoint = (point) => {
-  selectedPoint.value = point
-  // 加载该点已有阈值(演示用,模拟从后端加载)
-  if (point.id === 'MP-1002') {
-    thresholds.value = { tilt: { warning: 8, critical: 15 }, water: { warning: 35, critical: 55 }, vibration: { warning: 80, critical: 120 }, gas: { warning: 15, critical: 25 }, battery: { warning: 20 }, signal: { warning: 30 } }
-  } else {
-    thresholds.value = { tilt: { warning: 5, critical: 10 }, water: { warning: 25, critical: 40 }, vibration: { warning: 60, critical: 100 }, gas: { warning: 10, critical: 20 }, battery: { warning: 20 }, signal: { warning: 30 } }
-  }
+function getOnlineTagType(status) {
+  return Number(status) === 1 ? 'success' : 'info'
+}
+
+function getAlarmText(point) {
+  return Number(point?.equipmentStatus?.alarmStatus) === 1 ? '报警' : '正常'
 }
 
-const historyChartOption = computed(() => {
-  const point = selectedPoint.value
-  if (!point) return {}
-  let data = []
-  let yName = ''
-  let color = '#409eff'
-  switch (historyMetric.value) {
-    case 'tilt': data = point.historyData.tiltHistory; yName = '倾斜角度 (°)'; color = '#e6a23c'; break
-    case 'water': data = point.historyData.waterHistory; yName = '水位深度 (cm)'; color = '#3b82f6'; break
-    case 'vibration': data = point.historyData.vibrationHistory; yName = '震动强度 (mg)'; color = '#f56c6c'; break
-    case 'gas': data = point.historyData.gasHistory; yName = '气体浓度 (ppm)'; color = '#9b59b6'; break
+function getAlarmTagType(point) {
+  return Number(point?.equipmentStatus?.alarmStatus) === 1 ? 'danger' : 'success'
+}
+
+function getUseStatusText(status) {
+  const map = {
+    1: '在用',
+    2: '闲置',
+    3: '维修',
+    4: '报废',
+    5: '待入库'
   }
-  const days = data.map((_, idx) => `Day ${idx + 1}`)
-  return {
-    tooltip: { trigger: 'axis' },
-    xAxis: { type: 'category', data: days, axisLabel: { rotate: 45, interval: 5 } },
-    yAxis: { type: 'value', name: yName },
-    series: [{ data, type: 'line', smooth: true, lineStyle: { color, width: 2 }, areaStyle: { opacity: 0.1, color }, symbol: 'circle', symbolSize: 6 }],
-    grid: { containLabel: true, left: '10%' }
+  return map[Number(status)] || '未知'
+}
+
+function getUseStatusTagType(status) {
+  const map = {
+    1: 'success',
+    2: 'info',
+    3: 'warning',
+    4: 'danger',
+    5: ''
   }
-})
+  return map[Number(status)] || 'info'
+}
 
-const getMetricName = (metric) => {
-  const map = { tilt: '倾斜角度', water: '水位深度', vibration: '震动强度', gas: '气体浓度' }
-  return map[metric]
+function getBinaryAlarmText(status) {
+  return `${status}` === '1' ? '报警' : `${status}` === '0' ? '正常' : '-'
 }
 
-const getSuggestedThreshold = () => {
-  const metric = historyMetric.value
-  if (metric === 'tilt') return `${suggested.value.tilt.suggestedWarning}°`
-  if (metric === 'water') return `${suggested.value.water.suggestedCritical}cm`
-  if (metric === 'vibration') return `${selectedPoint.value?.historyData?.maxVibration || 80}mg`
-  return '科学建议值'
+function appendUnit(value, unit) {
+  if (value === undefined || value === null || value === '') return '-'
+  return `${value}${unit}`
 }
 
-const applySuggestedThreshold = () => {
-  const metric = historyMetric.value
-  if (metric === 'tilt') thresholds.value.tilt.warning = suggested.value.tilt.suggestedWarning
-  if (metric === 'water') thresholds.value.water.critical = suggested.value.water.suggestedCritical
-  if (metric === 'vibration') thresholds.value.vibration.critical = (selectedPoint.value?.historyData?.maxVibration || 80) + 10
-  if (metric === 'gas') thresholds.value.gas.warning = 12
-  ElMessage.success('已应用AI建议阈值')
+function formatDateTime(value) {
+  return value || '-'
 }
 
-const resetThresholds = () => {
-  selectPoint(selectedPoint.value)
-  ElMessage.info('已重置为当前监测点默认阈值')
+async function loadDeviceList() {
+  listLoading.value = true
+  try {
+    const res = await getManholeAlarmDeviceList({
+      isQueryManholeData: true
+    })
+    const list = Array.isArray(res?.data) ? res.data : []
+    deviceList.value = list
+
+    if (!list.length) {
+      selectedPoint.value = null
+      selectedDetail.value = null
+      thresholds.value = createEmptyThresholds()
+      return
+    }
+
+    const currentId = selectedPoint.value?.equipmentId
+    const currentPoint = list.find(item => item.equipmentId === currentId) || list[0]
+    await selectPoint(currentPoint)
+  } catch (error) {
+    deviceList.value = []
+    selectedPoint.value = null
+    selectedDetail.value = null
+  } finally {
+    listLoading.value = false
+  }
 }
 
-const saveThresholds = () => {
-  ElMessage.success('阈值配置已保存,将即时生效')
+async function selectPoint(point) {
+  if (!point?.equipmentId) return
+  selectedPoint.value = point
+  await loadDeviceDetail(point.equipmentId)
 }
 
-const exportConfig = () => {
-  ElMessage.info('导出阈值配置文件(模拟)')
+async function loadDeviceDetail(id) {
+  detailLoading.value = true
+  try {
+    const res = await getManholeAlarmDeviceDetail(id)
+    selectedDetail.value = res?.data || null
+    thresholds.value = normalizeThresholdList(selectedDetail.value?.warningThresholdList)
+  } catch (error) {
+    selectedDetail.value = null
+    thresholds.value = createEmptyThresholds()
+  } finally {
+    detailLoading.value = false
+  }
+}
+
+async function resetThresholds() {
+  if (!selectedPoint.value?.equipmentId) return
+  await loadDeviceDetail(selectedPoint.value.equipmentId)
+  ElMessage.success('已恢复为服务端最新阈值')
 }
 
-const applyAllThresholds = () => {
-  ElMessage.success('已对所有监测点应用推荐阈值(演示)')
+function hasThresholdValue(item) {
+  return (
+    (item.minValue !== null && item.minValue !== undefined) ||
+    (item.maxValue !== null && item.maxValue !== undefined)
+  )
+}
+
+async function saveThresholds() {
+  if (!selectedDetail.value?.equipmentCode) {
+    ElMessage.warning('未获取到设备编码,无法保存')
+    return
+  }
+
+  const invalidConfigs = thresholdConfigList.filter((config) => {
+    const formItem = thresholds.value[config.key]
+    return formItem.id && !hasThresholdValue(formItem)
+  })
+
+  if (invalidConfigs.length) {
+    ElMessage.warning(`${invalidConfigs[0].label}请至少保留一个阈值`)
+    return
+  }
+
+  const payloads = thresholdConfigList
+    .map((config) => {
+      const formItem = thresholds.value[config.key]
+      if (!hasThresholdValue(formItem) && !formItem.id) return null
+      return {
+        id: formItem.id || undefined,
+        deviceCode: selectedDetail.value.equipmentCode,
+        warningType: config.warningType,
+        warningCode: config.warningCode,
+        minValue: formItem.minValue,
+        maxValue: formItem.maxValue,
+        remark: formItem.remark || ''
+      }
+    })
+    .filter(Boolean)
+
+  if (!payloads.length) {
+    ElMessage.warning('请先填写至少一项阈值')
+    return
+  }
+
+  saveLoading.value = true
+  try {
+    await Promise.all(payloads.map(item => saveManholeAlarmThreshold(item)))
+    ElMessage.success('阈值保存成功')
+    await loadDeviceDetail(selectedPoint.value.equipmentId)
+  } finally {
+    saveLoading.value = false
+  }
 }
 
-import { ElMessage } from 'element-plus'
+onMounted(() => {
+  loadDeviceList()
+})
 </script>
 
 <style scoped>
@@ -391,99 +441,198 @@ import { ElMessage } from 'element-plus'
   background: #f0f2f6;
   min-height: 100vh;
 }
-.page-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  background: white;
-  padding: 16px 24px;
-  border-radius: 16px;
-  margin-bottom: 20px;
-}
-.header-left h2 { margin: 0 0 6px 0; font-weight: 600; }
+
 .main-content {
   display: flex;
   gap: 20px;
 }
+
 .point-list-panel {
-  width: 320px;
-  background: white;
+  width: 340px;
+  background: #fff;
   border-radius: 16px;
   padding: 16px;
   display: flex;
   flex-direction: column;
+  gap: 12px;
 }
+
 .panel-title {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  margin-bottom: 16px;
   font-weight: 600;
 }
+
 .point-list {
-  max-height: calc(100vh - 200px);
+  max-height: calc(100vh - 180px);
   overflow-y: auto;
 }
+
 .point-item {
+  border: 1px solid #e5e7eb;
+  border-radius: 12px;
   padding: 14px;
   margin-bottom: 12px;
-  border-radius: 12px;
   cursor: pointer;
-  transition: all 0.2s;
-  border: 1px solid #e4e7ed;
-}
-.point-item:hover { background: #f5f7fa; transform: translateX(2px); }
-.point-item.active { background: #ecf5ff; border-color: #409eff; }
-.point-header { display: flex; justify-content: space-between; margin-bottom: 6px; }
-.point-name { font-weight: 600; }
-.point-location { font-size: 12px; color: #6c757d; display: flex; align-items: center; gap: 4px; margin-bottom: 6px; }
-.point-device { font-size: 12px; color: #409eff; }
+  transition: all 0.2s ease;
+}
+
+.point-item:hover {
+  background: #f8fbff;
+  border-color: #bfdbfe;
+}
+
+.point-item.active {
+  background: #ecf5ff;
+  border-color: #409eff;
+}
+
+.point-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 8px;
+}
+
+.point-name {
+  font-weight: 600;
+  color: #303133;
+}
+
+.point-location {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  color: #606266;
+  font-size: 12px;
+  margin-bottom: 8px;
+}
+
+.point-meta {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  font-size: 12px;
+  color: #409eff;
+  margin-bottom: 8px;
+}
+
+.point-status-row {
+  display: flex;
+  gap: 8px;
+}
+
 .config-panel {
   flex: 1;
   display: flex;
   flex-direction: column;
   gap: 20px;
 }
-.info-card, .threshold-card, .history-chart-card {
-  background: white;
+
+.info-card,
+.threshold-card {
+  background: #fff;
   border-radius: 16px;
-  padding: 16px 20px;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.04);
+  padding: 18px 20px;
+  box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
 }
+
 .card-title {
   display: flex;
   justify-content: space-between;
   align-items: center;
   margin-bottom: 16px;
+  padding-left: 12px;
+  border-left: 4px solid #409eff;
+  font-size: 16px;
   font-weight: 600;
+}
+
+.card-actions {
+  display: flex;
+  gap: 8px;
+}
+
+.status-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 12px;
+}
+
+.status-item {
+  background: #f8fafc;
+  border: 1px solid #edf2f7;
+  border-radius: 12px;
+  padding: 14px;
+}
+
+.status-label {
+  color: #909399;
+  font-size: 13px;
+  margin-bottom: 8px;
+}
+
+.status-value {
+  color: #303133;
   font-size: 16px;
-  border-left: 4px solid #409eff;
-  padding-left: 12px;
+  font-weight: 600;
 }
+
+.threshold-tip {
+  margin-bottom: 12px;
+  padding: 10px 12px;
+  border-radius: 8px;
+  background: #f4f9ff;
+  color: #606266;
+  font-size: 13px;
+}
+
 .threshold-item {
-  margin-bottom: 24px;
-  padding-bottom: 16px;
-  border-bottom: 1px solid #f0f0f0;
+  padding: 8px 4px 4px;
 }
+
 .item-label {
-  font-weight: 500;
-  margin-bottom: 10px;
+  margin-bottom: 16px;
+  color: #606266;
+  font-size: 14px;
+}
+
+.item-control {
   display: flex;
   align-items: center;
-  gap: 6px;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-bottom: 16px;
 }
-.item-control {
+
+.field-label {
+  color: #303133;
+  font-weight: 500;
+}
+
+.threshold-input {
+  width: 160px;
+}
+
+.unit {
+  color: #909399;
+  margin-right: 16px;
+}
+
+.saved-hint {
+  margin-top: 10px;
+  color: #909399;
+  font-size: 12px;
+}
+
+.empty-panel {
+  flex: 1;
   display: flex;
+  justify-content: center;
   align-items: center;
-  gap: 6px;
-  margin-bottom: 12px;
+  background: #fff;
+  border-radius: 16px;
 }
-.unit { margin-right: 12px; color: #6c757d; }
-.level-tag { font-size: 12px; padding: 2px 8px; border-radius: 12px; margin-left: 8px; }
-.level-tag.warning { background: #fdf6ec; color: #e6a23c; }
-.level-tag.critical { background: #fef0f0; color: #f56c6c; }
-.history-suggestion { background: #f8f9fc; padding: 8px 12px; border-radius: 8px; font-size: 13px; display: flex; align-items: center; gap: 8px; color: #606266; }
-.chart-container { height: 260px; margin: 12px 0; }
-.ai-suggestion { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
-.empty-panel { flex: 1; display: flex; align-items: center; justify-content: center; background: white; border-radius: 16px; }
-</style>
+</style>

+ 229 - 388
src/views/subSystem/manholeCover/caseHandling/circulation.vue

@@ -1,462 +1,303 @@
 <!--案件流转管理-->
 <template>
-  <div class="case-management">
-    <!-- 页面头部 -->
-    <div class="page-header">
-      <div class="header-stats">
-        <div class="stat" @click="filterStatus('')">
-          <div class="num">{{ totalCases }}</div>
+  <div class="min-h-screen bg-[#f0f2f6] p-5">
+    <div class="mb-5 rounded-[20px] bg-white px-6 py-4">
+      <div class="flex flex-wrap gap-6">
+        <div class="min-w-[120px] cursor-pointer rounded-2xl px-4 py-2 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === '' }" @click="filterStatus('')">
+          <div class="text-[26px] font-bold">{{ totalCases }}</div>
           <div>全部案件</div>
         </div>
-        <div class="stat" @click="filterStatus('pending')">
-          <div class="num warning">{{ pendingCount }}</div>
+        <div class="min-w-[120px] cursor-pointer rounded-2xl px-4 py-2 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === 'pending' }" @click="filterStatus('pending')">
+          <div class="text-[26px] font-bold text-[#e6a23c]">{{ pendingCount }}</div>
           <div>待核查</div>
         </div>
-        <div class="stat" @click="filterStatus('processing')">
-          <div class="num">{{ processingCount }}</div>
+        <div class="min-w-[120px] cursor-pointer rounded-2xl px-4 py-2 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === 'processing' }" @click="filterStatus('processing')">
+          <div class="text-[26px] font-bold">{{ processingCount }}</div>
           <div>办理中</div>
         </div>
-        <div class="stat" @click="filterStatus('completed')">
-          <div class="num">{{ completedCount }}</div>
+        <div class="min-w-[120px] cursor-pointer rounded-2xl px-4 py-2 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === 'completed' }" @click="filterStatus('completed')">
+          <div class="text-[26px] font-bold">{{ completedCount }}</div>
           <div>已办结</div>
         </div>
       </div>
     </div>
 
-    <div class="main-layout">
-      <!-- 左侧案件列表 -->
-      <div class="case-list-panel">
-        <div class="list-header">
+    <div class="flex gap-5">
+      <div class="w-[380px] flex flex-col overflow-hidden rounded-[20px] bg-white" v-loading="loading">
+        <div class="flex justify-between border-b border-[#e9ecef] px-4 py-4 font-600">
           <span>案件列表</span>
-          <el-input v-model="searchKeyword" placeholder="搜索案件编号/位置" prefix-icon="Search" clearable size="small" style="width: 160px" />
+          <el-input v-model="searchKeyword" placeholder="搜索案件编号/设备位置" :prefix-icon="Search" clearable size="small" class="w-[180px]" @keyup.enter="loadCases" />
         </div>
-        <div class="case-list">
+        <div class="flex-1 overflow-y-auto p-3">
           <div
-              v-for="caseItem in filteredCases"
-              :key="caseItem.id"
-              class="case-item"
-              :class="{ active: selectedCase?.id === caseItem.id, [caseItem.status]: true }"
-              @click="selectCase(caseItem)"
+            v-for="caseItem in paginatedCases"
+            :key="caseItem.caseId"
+            class="mb-2 flex cursor-pointer items-center gap-3 rounded-[14px] border border-transparent bg-[#fafbfc] p-3"
+            :class="{ 'border-[#409eff] bg-[#ecf5ff]': selectedCase?.caseId === caseItem.caseId }"
+            @click="selectCase(caseItem)"
           >
             <div class="case-level">
-              <el-tag :type="caseItem.level === 'critical' ? 'danger' : 'warning'" size="small">
-                {{ caseItem.level === 'critical' ? '严重' : '一般' }}
-              </el-tag>
+              <el-tag :type="caseItem.levelTag" size="small">{{ caseItem.levelText }}</el-tag>
             </div>
-            <div class="case-info">
-              <div class="case-title">{{ caseItem.code }}</div>
-              <div class="case-location"><el-icon><Location /></el-icon> {{ caseItem.locationShort }}</div>
-              <div class="case-time">{{ caseItem.alertTime }}</div>
+            <div class="flex-1">
+              <div class="mb-1 font-600">{{ caseItem.caseNo }}</div>
+              <div class="flex items-center gap-1 text-[12px] text-[#6c757d]"><el-icon><Location /></el-icon> 设备编号 {{ caseItem.deviceCode || '—' }}</div>
+              <div class="text-[11px] text-[#aaa]">{{ caseItem.createTimeText }}</div>
             </div>
             <div class="case-status">
-              <el-tag :type="getStatusType(caseItem.status)" size="small">{{ getStatusText(caseItem.status) }}</el-tag>
+              <el-tag :type="caseItem.statusTag" size="small">{{ caseItem.statusText }}</el-tag>
             </div>
           </div>
+          <el-empty v-if="!paginatedCases.length" description="暂无案件数据" :image-size="80" />
         </div>
-        <div class="pagination-area">
+        <div class="border-t border-[#eee] p-3 text-center">
           <el-pagination small layout="prev, pager, next" :total="filteredCases.length" :page-size="pageSize" v-model:current-page="currentPage" />
         </div>
       </div>
 
-      <!-- 右侧案件详情与核查操作区 -->
-      <div class="detail-panel" v-if="selectedCase">
-        <!-- 基本信息与地理位置双栏 -->
-        <div class="detail-two-col">
-          <div class="info-section">
-            <div class="section-title">
+      <div class="max-h-[calc(100vh-140px)] flex-1 overflow-y-auto rounded-[20px] bg-white p-5" v-if="selectedCase" v-loading="detailLoading">
+        <div class="mb-5 flex gap-5">
+          <div class="flex-1">
+            <div class="mb-[14px] flex items-center gap-2 text-[16px] font-600">
               <span><el-icon><InfoFilled /></el-icon> 案件信息</span>
-              <div class="task-role">
-                <el-select v-model="selectedRole" placeholder="指定督办角色" size="small" style="width: 140px">
-                  <el-option label="网格巡查员" value="inspector" />
-                  <el-option label="维修班组" value="repair" />
-                  <el-option label="应急中心" value="emergency" />
-                </el-select>
-                <el-button type="primary" size="small" @click="assignTask">指派督办</el-button>
-              </div>
             </div>
             <el-descriptions :column="1" border>
-              <el-descriptions-item label="案件编号">{{ selectedCase.code }}</el-descriptions-item>
-              <el-descriptions-item label="报警级别">
-                <el-tag :type="selectedCase.level === 'critical' ? 'danger' : 'warning'">{{ selectedCase.level === 'critical' ? '严重报警' : '一般报警' }}</el-tag>
+              <el-descriptions-item label="案件ID">{{ selectedCase.caseId || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="案件编号">{{ selectedCase.caseNo }}</el-descriptions-item>
+              <el-descriptions-item label="报警ID">{{ selectedCase.alarmId || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="案件等级">
+                <el-tag :type="selectedCase.levelTag">{{ selectedCase.levelText }}</el-tag>
               </el-descriptions-item>
-              <el-descriptions-item label="报警描述">{{ selectedCase.alertDesc }}</el-descriptions-item>
-              <el-descriptions-item label="报警时间">{{ selectedCase.alertTime }}</el-descriptions-item>
-              <el-descriptions-item label="当前状态">{{ getStatusText(selectedCase.status) }}</el-descriptions-item>
-              <el-descriptions-item label="指定督办角色">{{ selectedCase.assignRole || '未指定' }}</el-descriptions-item>
+              <el-descriptions-item label="案件来源">{{ selectedCase.sourceText }}</el-descriptions-item>
+              <el-descriptions-item label="案件描述">{{ selectedCase.caseDesc || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="案件状态">{{ selectedCase.statusText }}</el-descriptions-item>
+              <el-descriptions-item label="立案人ID">{{ selectedCase.fileUser || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="立案时间">{{ selectedCase.createTimeText }}</el-descriptions-item>
+              <el-descriptions-item label="核查人ID">{{ selectedCase.verifyUser || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="核查时间">{{ selectedCase.verifyTimeText }}</el-descriptions-item>
+              <el-descriptions-item label="办结时间">{{ selectedCase.finishTimeText }}</el-descriptions-item>
+              <el-descriptions-item label="处置结果">{{ selectedCase.handleResult || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="更新时间">{{ selectedCase.updateTimeText }}</el-descriptions-item>
             </el-descriptions>
           </div>
-          <div class="location-section">
-            <div class="section-title"><el-icon><MapLocation /></el-icon> 地理位置</div>
-            <div class="map-mini">
-              <div class="mock-map">
-                <el-icon :size="36" color="#f56c6c"><Location /></el-icon>
-                <div>{{ selectedCase.address }}</div>
-                <div class="coords">经度: {{ selectedCase.lng }}, 纬度: {{ selectedCase.lat }}</div>
-              </div>
-            </div>
-            <!-- 井盖设备信息 -->
-            <div class="device-info">
-              <div class="sub-title"><el-icon><Monitor /></el-icon> 井盖设备信息</div>
-              <el-descriptions :column="2" size="small" border>
-                <el-descriptions-item label="设备编号">{{ selectedCase.deviceId }}</el-descriptions-item>
-                <el-descriptions-item label="设备类型">{{ selectedCase.deviceType }}</el-descriptions-item>
-                <el-descriptions-item label="设备型号">{{ selectedCase.deviceModel }}</el-descriptions-item>
-                <el-descriptions-item label="安装日期">{{ selectedCase.installDate }}</el-descriptions-item>
-                <el-descriptions-item label="当前状态">{{ selectedCase.deviceStatus }}</el-descriptions-item>
-                <el-descriptions-item label="最近通讯">{{ selectedCase.lastComm }}</el-descriptions-item>
-              </el-descriptions>
-            </div>
-          </div>
-        </div>
-
-        <!-- 报警多媒体信息 -->
-        <div class="media-section">
-          <div class="section-title"><el-icon><Picture /></el-icon> 报警多媒体信息</div>
-          <div class="media-gallery">
-            <el-image v-for="(img, idx) in selectedCase.mediaImages" :key="idx" :src="img" fit="cover" class="media-img" />
-            <div v-if="selectedCase.videoUrl" class="video-preview" @click="playVideo">▶ 查看现场视频</div>
-            <div v-if="!selectedCase.mediaImages?.length && !selectedCase.videoUrl" class="no-media">暂无多媒体附件</div>
+          <div class="flex-1">
+            <div class="mb-[14px] flex items-center gap-2 text-[16px] font-600"><el-icon><Monitor /></el-icon> 设备信息</div>
+            <el-descriptions :column="1" border>
+              <el-descriptions-item label="设备ID">{{ selectedCase.deviceId || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="设备编号">{{ selectedCase.deviceCode || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="设备类别">{{ selectedCase.equipmentTypeName || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="运维状态">{{ selectedCase.currentStatusText }}</el-descriptions-item>
+              <el-descriptions-item label="在线状态">{{ selectedCase.onlineStatusText }}</el-descriptions-item>
+              <el-descriptions-item label="报警状态">{{ selectedCase.alarmStatusText }}</el-descriptions-item>
+              <el-descriptions-item label="状态更新时间">{{ selectedCase.statusUpdateTimeText }}</el-descriptions-item>
+            </el-descriptions>
           </div>
         </div>
 
-        <!-- 办理经过 & 处置信息时间轴 -->
-        <div class="process-section">
-          <div class="section-title"><el-icon><Document /></el-icon> 办理经过 & 处置信息</div>
-          <el-timeline>
-            <el-timeline-item v-for="process in selectedCase.processLogs" :key="process.id" :timestamp="process.time" :type="process.type">
-              {{ process.content }}
-              <span v-if="process.operator" class="operator">操作人: {{ process.operator }}</span>
-            </el-timeline-item>
-          </el-timeline>
-        </div>
-
-        <!-- 核查操作栏 (任务督办核心) -->
-        <div class="action-bar">
-          <div class="section-title"><el-icon><Operation /></el-icon> 案件处置操作</div>
-          <div class="action-buttons" v-if="selectedCase.status !== 'completed'">
-            <el-button type="primary" @click="startVerification" :disabled="selectedCase.status !== 'pending'">核查</el-button>
-            <el-button type="success" @click="completeCase" :disabled="selectedCase.status === 'pending'">办结</el-button>
-            <el-button type="warning" @click="redispatchCase" :disabled="selectedCase.status === 'pending'">再次派遣</el-button>
-            <el-button @click="viewTaskDetail">查看处理详情</el-button>
+        <div class="mt-6 border-t border-[#e9ecef] pt-4">
+          <div class="mb-[14px] flex items-center gap-2 text-[16px] font-600"><el-icon><Picture /></el-icon> 案件附件</div>
+          <div class="flex flex-wrap gap-3" v-if="selectedCase.attachImages.length">
+            <el-image
+              v-for="(img, idx) in selectedCase.attachImages"
+              :key="idx"
+              :src="img"
+              fit="cover"
+              class="h-[80px] w-[100px] cursor-pointer rounded-lg object-cover"
+              :preview-src-list="selectedCase.attachImages"
+              preview-teleported
+            />
           </div>
-          <div v-else class="completed-info">案件已于 {{ selectedCase.completeTime }} 办结,全程督办记录可查。</div>
+          <el-empty v-else description="暂无案件附件" :image-size="80" />
         </div>
       </div>
 
-      <!-- 未选中案件的空状态 -->
-      <div v-else class="empty-panel">
-        <el-empty description="请从左侧选择案件进行核查与督办" :image-size="140" />
+      <div v-else class="flex flex-1 items-center justify-center rounded-[20px] bg-white">
+        <el-empty description="请从左侧选择案件进行核查" :image-size="140" />
       </div>
     </div>
-
-    <!-- 处理详情弹窗 -->
-    <el-dialog v-model="detailDialogVisible" title="任务处理详情 - 全程督办记录" width="600px">
-      <el-timeline>
-        <el-timeline-item v-for="log in selectedCase?.processLogs" :key="log.id" :timestamp="log.time" :type="log.type">
-          {{ log.content }} <span v-if="log.operator" class="operator-dialog">【{{ log.operator }}】</span>
-        </el-timeline-item>
-      </el-timeline>
-      <template #footer>
-        <el-button @click="detailDialogVisible = false">关闭</el-button>
-      </template>
-    </el-dialog>
-
-    <!-- 再次派遣对话框 -->
-    <el-dialog v-model="redispatchDialogVisible" title="再次派遣" width="400px">
-      <el-form>
-        <el-form-item label="指派角色">
-          <el-select v-model="newAssignRole" placeholder="选择角色">
-            <el-option label="网格巡查员" value="inspector" />
-            <el-option label="维修班组" value="repair" />
-            <el-option label="应急中心" value="emergency" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="派遣备注">
-          <el-input type="textarea" v-model="dispatchRemark" placeholder="请输入派遣说明" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="redispatchDialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="confirmRedispatch">确认派遣</el-button>
-      </template>
-    </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
-import { Tickets, Location, InfoFilled, MapLocation, Monitor, Picture, Document, Operation, Search } from '@element-plus/icons-vue'
-import { ElMessage } from 'element-plus'
-
-// 模拟案件数据
-const mockCases = [
-  {
-    id: 1, code: 'CASE-20250402-001', level: 'critical', alertDesc: '井盖倾斜角度14.2°,超过阈值8°,有跌落风险', alertTime: '2025-04-02 14:23:15',
-    locationShort: '人民路与解放路口', address: '人民路与解放路口东50m', lng: 121.487, lat: 31.249,
-    deviceId: 'MN-1001', deviceType: '智能井盖监测仪', deviceModel: 'WATCHMAN-S2', installDate: '2024-03-15', deviceStatus: '倾斜报警', lastComm: '2025-04-02 14:20',
-    mediaImages: ['https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'],
-    videoUrl: null,
-    status: 'pending',  // pending, processing, completed
-    assignRole: null,
-    processLogs: [
-      { id: 1, time: '2025-04-02 14:23:20', content: '系统自动生成报警案件', type: 'danger', operator: '系统' }
-    ],
-    completeTime: null
-  },
-  {
-    id: 2, code: 'CASE-20250402-002', level: 'critical', alertDesc: '井下水位62cm,超过阈值40cm,溢水风险', alertTime: '2025-04-02 13:58:12',
-    locationShort: '滨江路化工园区', address: '滨江路化工园区南门', lng: 121.502, lat: 31.235,
-    deviceId: 'MN-1002', deviceType: '防爆井盖监测仪', deviceModel: 'WATCHMAN-EX', installDate: '2024-06-20', deviceStatus: '水位超限', lastComm: '2025-04-02 13:55',
-    mediaImages: [],
-    videoUrl: 'demo.mp4',
-    status: 'processing',
-    assignRole: 'repair',
-    processLogs: [
-      { id: 1, time: '2025-04-02 13:58:20', content: '系统自动生成报警案件', type: 'danger', operator: '系统' },
-      { id: 2, time: '2025-04-02 14:10:00', content: '已指派维修班组现场勘查', type: 'warning', operator: '调度中心-李工' },
-      { id: 3, time: '2025-04-02 14:30:15', content: '维修人员已到达现场,开始排水作业', type: 'info', operator: '维修班组-张三' }
-    ],
-    completeTime: null
-  },
-  {
-    id: 3, code: 'CASE-20250402-003', level: 'warning', alertDesc: '异常震动112mg,超过阈值80mg,疑似外力冲击', alertTime: '2025-04-02 12:10:33',
-    locationShort: '古城街北段', address: '古城街北段树下', lng: 121.478, lat: 31.258,
-    deviceId: 'MN-1003', deviceType: '普通井盖监测仪', deviceModel: 'WATCHMAN-Basic', installDate: '2023-11-10', deviceStatus: '震动报警', lastComm: '2025-04-02 12:08',
-    mediaImages: ['https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg'],
-    videoUrl: null,
-    status: 'completed',
-    assignRole: 'inspector',
-    processLogs: [
-      { id: 1, time: '2025-04-02 12:10:40', content: '系统自动生成报警案件', type: 'danger', operator: '系统' },
-      { id: 2, time: '2025-04-02 12:35:00', content: '网格员现场核查,确认为重型车辆碾轧导致,井盖轻微移位', type: 'primary', operator: '巡查员-王明' },
-      { id: 3, time: '2025-04-02 13:20:00', content: '维修人员复位井盖,异常消除', type: 'success', operator: '维修班组' },
-      { id: 4, time: '2025-04-02 14:00:00', content: '案件办结', type: 'success', operator: '系统' }
-    ],
-    completeTime: '2025-04-02 14:00:00'
-  },
-  {
-    id: 4, code: 'CASE-20250402-004', level: 'warning', alertDesc: '井盖异常开启,非正常打开', alertTime: '2025-04-02 11:45:20',
-    locationShort: '南港路旧车站', address: '南港路旧车站段', lng: 121.495, lat: 31.228,
-    deviceId: 'MN-1005', deviceType: '智能井盖监测仪', deviceModel: 'WATCHMAN-S2', installDate: '2024-01-18', deviceStatus: '开启报警', lastComm: '2025-04-02 11:44',
-    mediaImages: [],
-    videoUrl: null,
-    status: 'pending',
-    assignRole: null,
-    processLogs: [{ id: 1, time: '2025-04-02 11:45:25', content: '系统自动生成报警案件', type: 'danger', operator: '系统' }],
-    completeTime: null
-  }
-]
+import {computed, onMounted, ref} from 'vue'
+import {InfoFilled, Location, Monitor, Picture, Search} from '@element-plus/icons-vue'
+import {getManholeCaseDetail, getManholeCasePage} from '@/api/pipeNetwork/basic'
 
+const cases = ref([])
+const loading = ref(false)
+const detailLoading = ref(false)
 const searchKeyword = ref('')
 const statusFilter = ref('')
 const currentPage = ref(1)
 const pageSize = 5
-const selectedCase = ref(mockCases[0])
-const selectedRole = ref('')
-const detailDialogVisible = ref(false)
-const redispatchDialogVisible = ref(false)
-const newAssignRole = ref('')
-const dispatchRemark = ref('')
+const selectedCase = ref(null)
 
-const filteredCases = computed(() => {
-  let list = mockCases
-  if (statusFilter.value) list = list.filter(c => c.status === statusFilter.value)
-  if (searchKeyword.value) {
-    const kw = searchKeyword.value.toLowerCase()
-    list = list.filter(c => c.code.toLowerCase().includes(kw) || c.locationShort.toLowerCase().includes(kw))
-  }
-  return list
-})
+function formatDateTime(value) {
+  if (!value) return '—'
+  return String(value).replace('T', ' ')
+}
 
-const totalCases = computed(() => mockCases.length)
-const pendingCount = computed(() => mockCases.filter(c => c.status === 'pending').length)
-const processingCount = computed(() => mockCases.filter(c => c.status === 'processing').length)
-const completedCount = computed(() => mockCases.filter(c => c.status === 'completed').length)
+function getCaseLevelMeta(level) {
+  const map = {
+    1: { text: '严重', tag: 'danger' },
+    2: { text: '一般', tag: 'warning' },
+    3: { text: '轻微', tag: 'info' }
+  }
+  return map[Number(level)] || { text: '—', tag: 'info' }
+}
 
-const filterStatus = (status) => { statusFilter.value = status; currentPage.value = 1 }
-const selectCase = (caseItem) => { selectedCase.value = caseItem; selectedRole.value = caseItem.assignRole || '' }
-const getStatusType = (status) => {
-  if (status === 'pending') return 'danger'
-  if (status === 'processing') return 'warning'
-  return 'success'
+function getCaseStatusMeta(status) {
+  const map = {
+    1: { key: 'pending', text: '待核查', tag: 'danger' },
+    2: { key: 'processing', text: '办理中', tag: 'warning' },
+    3: { key: 'completed', text: '已办结', tag: 'success' },
+    4: { key: 'closed', text: '误报关闭', tag: 'info' }
+  }
+  return map[Number(status)] || { key: '', text: '—', tag: 'info' }
 }
-const getStatusText = (status) => {
-  if (status === 'pending') return '待核查'
-  if (status === 'processing') return '办理中'
-  return '已办结'
+
+function getCaseSourceText(source) {
+  const map = {
+    1: '系统自动报警',
+    2: '运维巡检上报',
+    3: '人工投诉上报',
+    4: '平台录入'
+  }
+  return map[Number(source)] || '—'
 }
 
-// 核查操作
-const startVerification = () => {
-  if (selectedCase.value.status !== 'pending') return
-  selectedCase.value.status = 'processing'
-  selectedCase.value.processLogs.push({
-    id: Date.now(),
-    time: new Date().toLocaleString(),
-    content: '开始核查,已指派人员现场核实',
-    type: 'primary',
-    operator: '核查中心'
-  })
-  ElMessage.success('已启动核查,案件状态更新为办理中')
+function splitAttachUrls(value) {
+  if (!value) return []
+  return String(value)
+    .split(/[,\n;]/)
+    .map(item => item.trim())
+    .filter(Boolean)
 }
 
-// 办结操作
-const completeCase = () => {
-  if (selectedCase.value.status === 'completed') return
-  selectedCase.value.status = 'completed'
-  selectedCase.value.completeTime = new Date().toLocaleString()
-  selectedCase.value.processLogs.push({
-    id: Date.now(),
-    time: new Date().toLocaleString(),
-    content: '案件办结,已完成处置并验收',
-    type: 'success',
-    operator: '督办员'
-  })
-  ElMessage.success('案件已办结')
+function getOnlineStatusText(status) {
+  const map = {
+    0: '离线',
+    1: '在线'
+  }
+  return map[Number(status)] || '—'
 }
 
-// 再次派遣
-const redispatchCase = () => {
-  redispatchDialogVisible.value = true
+function getAlarmStatusText(status) {
+  const map = {
+    0: '正常',
+    1: '报警'
+  }
+  return map[Number(status)] || '—'
 }
-const confirmRedispatch = () => {
-  if (newAssignRole.value) {
-    selectedCase.value.assignRole = newAssignRole.value
-    selectedCase.value.processLogs.push({
-      id: Date.now(),
-      time: new Date().toLocaleString(),
-      content: `再次派遣至角色: ${newAssignRole.value === 'inspector' ? '网格巡查员' : newAssignRole.value === 'repair' ? '维修班组' : '应急中心'},备注: ${dispatchRemark.value || '无'}`,
-      type: 'warning',
-      operator: '督办中心'
-    })
-    ElMessage.success('再次派遣成功,已通知对应角色')
-  } else {
-    ElMessage.warning('请选择指派角色')
+
+function getCurrentStatusText(status) {
+  const map = {
+    1: '在用',
+    2: '闲置',
+    3: '维修',
+    4: '报废',
+    5: '待入库'
   }
-  redispatchDialogVisible.value = false
-  newAssignRole.value = ''
-  dispatchRemark.value = ''
+  return map[Number(status)] || '—'
 }
 
-const assignTask = () => {
-  if (!selectedRole.value) {
-    ElMessage.warning('请选择督办角色')
-    return
+function normalizeCase(item) {
+  const levelMeta = getCaseLevelMeta(item.caseLevel)
+  const statusMeta = getCaseStatusMeta(item.caseStatus)
+  return {
+    ...item,
+    levelText: levelMeta.text,
+    levelTag: levelMeta.tag,
+    statusKey: statusMeta.key,
+    statusText: statusMeta.text,
+    statusTag: statusMeta.tag,
+    sourceText: getCaseSourceText(item.caseSource),
+    createTimeText: formatDateTime(item.createTime),
+    verifyTimeText: formatDateTime(item.verifyTime),
+    finishTimeText: formatDateTime(item.finishTime),
+    updateTimeText: formatDateTime(item.updateTime),
+    attachImages: splitAttachUrls(item.attachUrl),
+    currentStatusText: getCurrentStatusText(item.equipmentStatus?.currentStatus),
+    onlineStatusText: getOnlineStatusText(item.equipmentStatus?.onlineStatus),
+    alarmStatusText: getAlarmStatusText(item.equipmentStatus?.alarmStatus),
+    statusUpdateTimeText: formatDateTime(item.equipmentStatus?.statusUpdateTime)
   }
-  selectedCase.value.assignRole = selectedRole.value
-  selectedCase.value.processLogs.push({
-    id: Date.now(),
-    time: new Date().toLocaleString(),
-    content: `已指派角色 ${selectedRole.value === 'inspector' ? '网格巡查员' : selectedRole.value === 'repair' ? '维修班组' : '应急中心'} 进行全程督办`,
-    type: 'primary',
-    operator: '系统'
-  })
-  ElMessage.success(`已指定${selectedRole.value}角色进行任务督办`)
 }
 
-const viewTaskDetail = () => { detailDialogVisible.value = true }
-const playVideo = () => { ElMessage.info('视频播放功能演示') }
+const filteredCases = computed(() => {
+  if (!statusFilter.value) return cases.value
+  return cases.value.filter(item => item.statusKey === statusFilter.value)
+})
 
-</script>
+const paginatedCases = computed(() => {
+  const start = (currentPage.value - 1) * pageSize
+  return filteredCases.value.slice(start, start + pageSize)
+})
 
-<style scoped>
-.case-management {
-  background: #f0f2f6;
-  min-height: 100vh;
-  padding: 20px;
-}
-.page-header {
-  background: white;
-  border-radius: 20px;
-  padding: 16px 24px;
-  margin-bottom: 20px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-.header-title { display: flex; align-items: center; gap: 14px; font-size: 20px; font-weight: 600; }
-.header-stats { display: flex; gap: 24px; }
-.stat { text-align: center; cursor: pointer; padding: 4px 16px; border-radius: 30px; }
-.stat:hover { background: #ecf5ff; }
-.num { font-size: 26px; font-weight: bold; }
-.num.warning { color: #e6a23c; }
-.main-layout { display: flex; gap: 20px; }
-.case-list-panel {
-  width: 380px;
-  background: white;
-  border-radius: 20px;
-  display: flex;
-  flex-direction: column;
-  overflow: hidden;
-}
-.list-header {
-  padding: 16px;
-  border-bottom: 1px solid #e9ecef;
-  display: flex;
-  justify-content: space-between;
-  font-weight: 600;
-}
-.case-list { flex: 1; overflow-y: auto; padding: 12px; }
-.case-item {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  padding: 12px;
-  margin-bottom: 8px;
-  border-radius: 14px;
-  background: #fafbfc;
-  cursor: pointer;
-  border: 1px solid transparent;
-}
-.case-item.active { background: #ecf5ff; border-color: #409eff; }
-.case-item.pending { border-left: 3px solid #f56c6c; }
-.case-item.processing { border-left: 3px solid #e6a23c; }
-.case-info { flex: 1; }
-.case-title { font-weight: 600; margin-bottom: 4px; }
-.case-location { font-size: 12px; color: #6c757d; display: flex; align-items: center; gap: 4px; }
-.case-time { font-size: 11px; color: #aaa; }
-.detail-panel {
-  flex: 1;
-  background: white;
-  border-radius: 20px;
-  padding: 20px;
-  overflow-y: auto;
-  max-height: calc(100vh - 140px);
-}
-.detail-two-col { display: flex; gap: 20px; margin-bottom: 20px; }
-.info-section { flex: 1; }
-.location-section { flex: 1; }
-.section-title {
-  font-weight: 600;
-  font-size: 16px;
-  margin-bottom: 14px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
+const totalCases = computed(() => cases.value.length)
+const pendingCount = computed(() => cases.value.filter(item => item.statusKey === 'pending').length)
+const processingCount = computed(() => cases.value.filter(item => item.statusKey === 'processing').length)
+const completedCount = computed(() => cases.value.filter(item => item.statusKey === 'completed').length)
+
+async function loadCases() {
+  loading.value = true
+  try {
+    const keyword = searchKeyword.value.trim()
+    const res = await getManholeCasePage({
+      pageNum: 1,
+      pageSize: 200,
+      caseNo: keyword || undefined,
+      equipmentLocation: keyword || undefined
+    })
+    const records = Array.isArray(res?.data?.records) ? res.data.records : []
+    cases.value = records.map(item => normalizeCase(item))
+    currentPage.value = 1
+    const first = filteredCases.value[0]
+    if (!first) {
+      selectedCase.value = null
+      return
+    }
+    if (selectedCase.value) {
+      const matched = cases.value.find(item => item.caseId === selectedCase.value.caseId)
+      if (matched) {
+        await selectCase(matched)
+        return
+      }
+    }
+    await selectCase(first)
+  } catch (error) {
+    cases.value = []
+    selectedCase.value = null
+  } finally {
+    loading.value = false
+  }
 }
-.mock-map {
-  background: #e9f5e9;
-  border-radius: 16px;
-  height: 130px;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  margin-bottom: 16px;
+
+async function selectCase(caseItem) {
+  detailLoading.value = true
+  try {
+    const res = await getManholeCaseDetail(caseItem.caseId)
+    const detail = res?.data || caseItem
+    selectedCase.value = normalizeCase(detail)
+  } catch (error) {
+    selectedCase.value = caseItem
+  } finally {
+    detailLoading.value = false
+  }
 }
-.coords { font-size: 11px; color: #666; margin-top: 4px; }
-.device-info { margin-top: 12px; }
-.sub-title { font-weight: 500; margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
-.media-section, .process-section, .action-bar {
-  margin-top: 24px;
-  border-top: 1px solid #e9ecef;
-  padding-top: 16px;
+
+function filterStatus(status) {
+  statusFilter.value = status
+  currentPage.value = 1
+  const first = filteredCases.value[0]
+  if (first) {
+    selectCase(first)
+  } else {
+    selectedCase.value = null
+  }
 }
-.media-gallery { display: flex; gap: 12px; flex-wrap: wrap; }
-.media-img { width: 100px; height: 80px; border-radius: 8px; object-fit: cover; cursor: pointer; }
-.video-preview { background: #f0f0f0; padding: 8px 16px; border-radius: 20px; cursor: pointer; display: inline-block; }
-.action-buttons { display: flex; gap: 12px; margin-top: 12px; }
-.empty-panel { flex: 1; display: flex; align-items: center; justify-content: center; background: white; border-radius: 20px; }
-.operator { font-size: 12px; color: #909399; margin-left: 12px; }
-.pagination-area { padding: 12px; text-align: center; border-top: 1px solid #eee; }
-</style>
+
+onMounted(() => {
+  loadCases()
+})
+</script>

+ 269 - 509
src/views/subSystem/manholeCover/caseHandling/closeWorkOrder.vue

@@ -1,597 +1,357 @@
 <!--办结工单-->
 <template>
-  <div class="completed-orders">
-    <!-- 页面头部 -->
-    <div class="page-header">
-      <div class="header-left">
+  <div class="min-h-screen bg-[#f0f2f6] p-5">
+    <div class="mb-5 flex flex-wrap items-center justify-between gap-3 rounded-2xl bg-white px-6 py-4 shadow-[0_2px_8px_rgba(0,0,0,0.04)]">
+      <div class="flex items-center gap-3">
         <el-icon><CircleCheck /></el-icon>
-        <span class="title">已办结工单</span>
+        <span class="text-[20px] font-600 text-[#1f2d3d]">已办结工单</span>
         <el-tag type="success" effect="dark">已完成 {{ completedOrdersCount }}</el-tag>
       </div>
-      <div class="header-right">
+      <div class="flex flex-wrap items-center gap-3">
         <el-input
-            v-model="searchKeyword"
-            placeholder="搜索工单编号/案件编号/地址"
-            clearable
-            :prefix-icon="Search"
-            style="width: 260px"
-            size="default"
+          v-model="searchKeyword"
+          placeholder="搜索工单编号/案件编号"
+          clearable
+          :prefix-icon="Search"
+          class="w-[260px]"
+          @keyup.enter="loadOrders"
         />
-        <el-select v-model="alertLevelFilter" placeholder="报警级别" clearable style="width: 120px" size="default">
-          <el-option label="严重" value="critical" />
-          <el-option label="一般" value="warning" />
+        <el-select v-model="orderLevelFilter" placeholder="优先级" clearable class="w-[120px]">
+          <el-option label="紧急" :value="1" />
+          <el-option label="一般" :value="2" />
+          <el-option label="低" :value="3" />
         </el-select>
         <el-date-picker
-            v-model="dateRange"
-            type="daterange"
-            range-separator="至"
-            start-placeholder="完成起始"
-            end-placeholder="完成结束"
-            size="default"
-            style="width: 260px"
+          v-model="dateRange"
+          type="daterange"
+          range-separator="至"
+          start-placeholder="办结开始"
+          end-placeholder="办结结束"
+          value-format="YYYY-MM-DD"
+          class="w-[280px]"
         />
-        <el-button type="primary" :icon="Refresh" @click="refreshList">刷新</el-button>
-        <el-button :icon="Download" @click="exportData">导出</el-button>
+        <el-button type="primary" :icon="Refresh" @click="loadOrders">刷新</el-button>
       </div>
     </div>
 
-    <!-- 统计卡片 -->
-    <div class="stats-cards">
-      <div class="stat-card" @click="setTimeFilter('week')" :class="{ active: timeFilter === 'week' }">
-        <div class="stat-value">{{ weeklyCompleted }}</div>
-        <div class="stat-label">本周办结</div>
-      </div>
-      <div class="stat-card" @click="setTimeFilter('month')" :class="{ active: timeFilter === 'month' }">
-        <div class="stat-value">{{ monthlyCompleted }}</div>
-        <div class="stat-label">本月办结</div>
+    <div class="mb-5 flex flex-wrap gap-4">
+      <div
+        class="min-w-[110px] cursor-pointer rounded-xl bg-white px-6 py-3 text-center shadow-[0_1px_4px_rgba(0,0,0,0.05)] transition-all duration-200 hover:-translate-y-[2px] hover:shadow-[0_4px_12px_rgba(0,0,0,0.1)]"
+        :class="timeFilter === 'week' ? 'border-2 border-[#67c23a] bg-[#f0f9f0]' : 'border-2 border-transparent'"
+        @click="setTimeFilter('week')"
+      >
+        <div class="text-[28px] font-bold text-[#2c3e50]">{{ weeklyCompleted }}</div>
+        <div class="mt-1 text-[13px] text-[#606266]">本周办结</div>
       </div>
-      <div class="stat-card" @click="setTimeFilter('all')" :class="{ active: timeFilter === 'all' }">
-        <div class="stat-value">{{ completedOrdersCount }}</div>
-        <div class="stat-label">全部办结</div>
+      <div
+        class="min-w-[110px] cursor-pointer rounded-xl bg-white px-6 py-3 text-center shadow-[0_1px_4px_rgba(0,0,0,0.05)] transition-all duration-200 hover:-translate-y-[2px] hover:shadow-[0_4px_12px_rgba(0,0,0,0.1)]"
+        :class="timeFilter === 'month' ? 'border-2 border-[#67c23a] bg-[#f0f9f0]' : 'border-2 border-transparent'"
+        @click="setTimeFilter('month')"
+      >
+        <div class="text-[28px] font-bold text-[#2c3e50]">{{ monthlyCompleted }}</div>
+        <div class="mt-1 text-[13px] text-[#606266]">本月办结</div>
       </div>
-      <div class="stat-card">
-        <div class="stat-value">{{ avgCompletionHours }}</div>
-        <div class="stat-label">平均办结时长(小时)</div>
+      <div
+        class="min-w-[110px] cursor-pointer rounded-xl bg-white px-6 py-3 text-center shadow-[0_1px_4px_rgba(0,0,0,0.05)] transition-all duration-200 hover:-translate-y-[2px] hover:shadow-[0_4px_12px_rgba(0,0,0,0.1)]"
+        :class="timeFilter === 'all' ? 'border-2 border-[#67c23a] bg-[#f0f9f0]' : 'border-2 border-transparent'"
+        @click="setTimeFilter('all')"
+      >
+        <div class="text-[28px] font-bold text-[#2c3e50]">{{ completedOrdersCount }}</div>
+        <div class="mt-1 text-[13px] text-[#606266]">全部办结</div>
       </div>
     </div>
 
-    <!-- 工单列表 -->
-    <div class="order-table-container">
-      <el-table
-          :data="paginatedOrders"
-          stripe
-          border
-          style="width: 100%"
-          v-loading="loading"
-          @row-click="handleRowClick"
-      >
+    <div class="rounded-2xl bg-white p-4 shadow-[0_2px_8px_rgba(0,0,0,0.04)]">
+      <el-table :data="paginatedOrders" stripe border class="w-full" row-class-name="cursor-pointer" v-loading="loading" @row-click="handleRowClick">
         <el-table-column type="index" label="序号" width="55" align="center" />
-        <el-table-column prop="code" label="工单编号" width="160" sortable />
-        <el-table-column prop="caseCode" label="关联案件编号" width="160" />
-        <el-table-column prop="alertLevel" label="报警级别" width="100" align="center">
-          <template #default="{ row }">
-            <el-tag :type="row.alertLevel === 'critical' ? 'danger' : 'warning'" size="small">
-              {{ row.alertLevel === 'critical' ? '严重' : '一般' }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="alertType" label="报警类型" width="120" />
-        <el-table-column prop="alertDesc" label="报警描述" min-width="180" show-overflow-tooltip />
-        <el-table-column prop="ownerUnit" label="权属单位" width="140" />
-        <el-table-column prop="address" label="详细地址" min-width="160" show-overflow-tooltip />
-        <el-table-column prop="assignee" label="处置人员" width="120" />
-        <el-table-column prop="completeTime" label="办结时间" width="160" sortable />
-        <el-table-column prop="completionHours" label="耗时(小时)" width="100" sortable align="center">
-          <template #default="{ row }">
-            <el-tag :type="row.completionHours <= 24 ? 'success' : row.completionHours <= 48 ? 'warning' : 'danger'" size="small">
-              {{ row.completionHours }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="满意度" width="120" align="center">
+        <el-table-column prop="orderNo" label="工单编号" min-width="170" />
+        <el-table-column prop="caseNo" label="关联案件编号" min-width="170" />
+        <el-table-column prop="orderLevelText" label="优先级" width="100" align="center">
           <template #default="{ row }">
-            <el-rate v-model="row.satisfaction" disabled :colors="['#99a9bf', '#f7ba2a', '#ff9900']" />
+            <el-tag :type="row.orderLevelTag" size="small">{{ row.orderLevelText }}</el-tag>
           </template>
         </el-table-column>
+        <el-table-column prop="orderTypeText" label="工单类型" width="110" />
+        <el-table-column prop="orderDesc" label="问题描述" min-width="220" show-overflow-tooltip />
+        <el-table-column prop="deviceCode" label="设备编号" width="150" />
+        <el-table-column prop="realFinishTimeText" label="办结时间" width="170" />
         <el-table-column label="操作" width="120" fixed="right" align="center">
           <template #default="{ row }">
             <el-button link type="primary" size="small" @click.stop="viewDetail(row)">查看详情</el-button>
-            <el-button link type="success" size="small" @click.stop="viewFeedback(row)">反馈详情</el-button>
           </template>
         </el-table-column>
       </el-table>
 
-      <!-- 分页 -->
-      <div class="pagination-wrapper">
+      <div class="mt-4 flex justify-end border-t border-[#e9ecef] pt-3">
         <el-pagination
-            background
-            layout="total, sizes, prev, pager, next, jumper"
-            :total="filteredOrders.length"
-            v-model:current-page="currentPage"
-            v-model:page-size="pageSize"
-            :page-sizes="[10, 20, 50, 100]"
+          background
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="filteredOrders.length"
+          v-model:current-page="currentPage"
+          v-model:page-size="pageSize"
+          :page-sizes="[10, 20, 50]"
         />
       </div>
     </div>
 
-    <!-- 工单详情抽屉 -->
-    <el-drawer v-model="detailDrawerVisible" :title="`工单详情 - ${currentOrder?.code}`" size="550px" direction="rtl">
-      <div v-if="currentOrder" class="order-detail">
+    <el-drawer v-model="detailDrawerVisible" :title="`工单详情 - ${currentOrder?.orderNo || ''}`" size="560px" direction="rtl">
+      <div v-if="currentOrder" class="p-2" v-loading="detailLoading">
         <el-descriptions :column="1" border>
-          <el-descriptions-item label="工单编号">{{ currentOrder.code }}</el-descriptions-item>
-          <el-descriptions-item label="关联案件">
-            <el-link type="primary" @click="viewCaseDetail">{{ currentOrder.caseCode }}</el-link>
+          <el-descriptions-item label="工单ID">{{ currentOrder.orderId || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="工单编号">{{ currentOrder.orderNo }}</el-descriptions-item>
+          <el-descriptions-item label="报警ID">{{ currentOrder.alarmId || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="案件ID">{{ currentOrder.caseId || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="关联案件编号">{{ currentOrder.caseNo || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="设备ID">{{ currentOrder.deviceId || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="设备编号">{{ currentOrder.deviceCode || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="工单状态">
+            <el-tag :type="currentOrder.orderStatusTag">{{ currentOrder.orderStatusText }}</el-tag>
           </el-descriptions-item>
-          <el-descriptions-item label="报警级别">
-            <el-tag :type="currentOrder.alertLevel === 'critical' ? 'danger' : 'warning'">
-              {{ currentOrder.alertLevel === 'critical' ? '严重' : '一般' }}
-            </el-tag>
+          <el-descriptions-item label="优先级">
+            <el-tag :type="currentOrder.orderLevelTag">{{ currentOrder.orderLevelText }}</el-tag>
           </el-descriptions-item>
-          <el-descriptions-item label="报警类型">{{ currentOrder.alertType }}</el-descriptions-item>
-          <el-descriptions-item label="报警描述">{{ currentOrder.alertDesc }}</el-descriptions-item>
-          <el-descriptions-item label="权属单位">{{ currentOrder.ownerUnit }}</el-descriptions-item>
-          <el-descriptions-item label="详细地址">{{ currentOrder.address }}</el-descriptions-item>
-          <el-descriptions-item label="用途">{{ currentOrder.purpose || '市政排水' }}</el-descriptions-item>
-          <el-descriptions-item label="处置人员">{{ currentOrder.assignee }}</el-descriptions-item>
-          <el-descriptions-item label="创建时间">{{ currentOrder.createTime }}</el-descriptions-item>
-          <el-descriptions-item label="办结时间">{{ currentOrder.completeTime }}</el-descriptions-item>
-          <el-descriptions-item label="处理耗时">{{ currentOrder.completionHours }} 小时</el-descriptions-item>
-          <el-descriptions-item label="满意度评分">
-            <el-rate v-model="currentOrder.satisfaction" disabled />
-          </el-descriptions-item>
-          <el-descriptions-item label="审批意见">{{ currentOrder.approvalRemark || '无' }}</el-descriptions-item>
+          <el-descriptions-item label="工单类型">{{ currentOrder.orderTypeText }}</el-descriptions-item>
+          <el-descriptions-item label="问题描述">{{ currentOrder.orderDesc || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="负责部门ID">{{ currentOrder.deptId || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="设备类别">{{ currentOrder.equipmentTypeName || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="接单人ID">{{ currentOrder.receiveUser || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="派单人ID">{{ currentOrder.dispatchUser || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="验收人ID">{{ currentOrder.verifyUser || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="派单时间">{{ currentOrder.dispatchTimeText }}</el-descriptions-item>
+          <el-descriptions-item label="创建时间">{{ currentOrder.createTimeText }}</el-descriptions-item>
+          <el-descriptions-item label="计划完成时间">{{ currentOrder.planFinishTimeText }}</el-descriptions-item>
+          <el-descriptions-item label="延期原因">{{ currentOrder.delayedReason || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="延期截止时间">{{ currentOrder.delayedDeadlineText }}</el-descriptions-item>
+          <el-descriptions-item label="验收时间">{{ currentOrder.verifyTimeText }}</el-descriptions-item>
+          <el-descriptions-item label="办结时间">{{ currentOrder.realFinishTimeText }}</el-descriptions-item>
+          <el-descriptions-item label="处置结果">{{ currentOrder.repairResult || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="设备运维状态">{{ currentOrder.currentStatusText }}</el-descriptions-item>
+          <el-descriptions-item label="设备在线状态">{{ currentOrder.onlineStatusText }}</el-descriptions-item>
+          <el-descriptions-item label="设备报警状态">{{ currentOrder.alarmStatusText }}</el-descriptions-item>
+          <el-descriptions-item label="状态更新时间">{{ currentOrder.statusUpdateTimeText }}</el-descriptions-item>
+          <el-descriptions-item label="更新时间">{{ currentOrder.updateTimeText }}</el-descriptions-item>
         </el-descriptions>
 
-        <!-- 处置反馈信息 -->
-        <div class="feedback-section" v-if="currentOrder.feedback">
-          <div class="sub-title">
-            <el-icon><Picture /></el-icon> 现场处置反馈
-          </div>
-          <el-descriptions :column="1" border size="small">
-            <el-descriptions-item label="反馈人员">{{ currentOrder.feedback.workerName }}</el-descriptions-item>
-            <el-descriptions-item label="反馈时间">{{ currentOrder.feedback.feedbackTime }}</el-descriptions-item>
-            <el-descriptions-item label="处置说明">{{ currentOrder.feedback.description }}</el-descriptions-item>
-            <el-descriptions-item label="现场照片">
-              <div class="feedback-images">
-                <el-image
-                    v-for="(img, idx) in currentOrder.feedback.images"
-                    :key="idx"
-                    :src="img"
-                    fit="cover"
-                    class="feedback-img"
-                    :preview-src-list="currentOrder.feedback.images"
-                    preview-teleported
-                />
-              </div>
-            </el-descriptions-item>
-          </el-descriptions>
-        </div>
-
-        <!-- 处理记录时间轴 -->
-        <div class="process-logs" v-if="currentOrder.processLogs?.length">
-          <div class="sub-title">办理经过</div>
-          <el-timeline>
-            <el-timeline-item v-for="log in currentOrder.processLogs" :key="log.id" :timestamp="log.time" :type="log.type">
-              {{ log.content }}
-              <span v-if="log.operator" class="operator">操作人: {{ log.operator }}</span>
+        <div class="mt-4">
+          <div class="mb-3 mt-5 text-[15px] font-600 text-[#2c3e50]">办理经过</div>
+          <el-timeline v-if="currentOrder.workOrderLogList?.length">
+            <el-timeline-item
+              v-for="log in currentOrder.workOrderLogList"
+              :key="log.logId"
+              :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>
             </el-timeline-item>
           </el-timeline>
+          <el-empty v-else description="暂无办理日志" :image-size="80" />
         </div>
       </div>
     </el-drawer>
-
-    <!-- 反馈详情弹窗 -->
-    <el-dialog v-model="feedbackDialogVisible" title="处置反馈详情" width="500px">
-      <div v-if="currentFeedback">
-        <el-descriptions :column="1" border>
-          <el-descriptions-item label="处置人员">{{ currentFeedback.workerName }}</el-descriptions-item>
-          <el-descriptions-item label="反馈时间">{{ currentFeedback.feedbackTime }}</el-descriptions-item>
-          <el-descriptions-item label="处置说明">{{ currentFeedback.description }}</el-descriptions-item>
-          <el-descriptions-item label="现场照片">
-            <div class="feedback-images">
-              <el-image
-                  v-for="(img, idx) in currentFeedback.images"
-                  :key="idx"
-                  :src="img"
-                  fit="cover"
-                  class="feedback-img"
-                  :preview-src-list="currentFeedback.images"
-                  preview-teleported
-              />
-            </div>
-          </el-descriptions-item>
-        </el-descriptions>
-      </div>
-    </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
-import { CircleCheck, Search, Refresh, Download, Picture } from '@element-plus/icons-vue'
-import { ElMessage } from 'element-plus'
-
-// 模拟已办结工单数据
-const mockCompletedOrders = ref([
-  {
-    id: 1, code: 'WO-20250401-001', caseCode: 'CASE-20250401-001', alertLevel: 'critical',
-    alertType: '倾斜超标', alertDesc: '井盖倾斜角度14.2°,超过阈值8°,有跌落风险',
-    ownerUnit: '市政工程管理处', address: '人民路与解放路口东50m', purpose: '主干道排水',
-    status: 'completed', createTime: '2025-04-01 09:30:00', completeTime: '2025-04-01 16:20:00',
-    completionHours: 6.8, assignee: '张建国', satisfaction: 5,
-    approvalRemark: '处置及时,井盖已修复加固',
-    feedback: {
-      workerName: '张建国', feedbackTime: '2025-04-01 15:30:00',
-      description: '现场井盖因车辆碾压移位,已复位加固,设备测试正常',
-      images: ['https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg']
-    },
-    processLogs: [
-      { id: 1, time: '2025-04-01 09:30:00', content: '工单自动创建', type: 'info', operator: '系统' },
-      { id: 2, time: '2025-04-01 10:00:00', content: '指派张建国前往处置', type: 'primary', operator: '调度中心' },
-      { id: 3, time: '2025-04-01 15:30:00', content: '提交处置反馈', type: 'warning', operator: '张建国' },
-      { id: 4, time: '2025-04-01 16:20:00', content: '审批通过,工单办结', type: 'success', operator: '审批人' }
-    ]
-  },
-  {
-    id: 2, code: 'WO-20250331-002', caseCode: 'CASE-20250331-002', alertLevel: 'critical',
-    alertType: '水位超限', alertDesc: '井下水位62cm,超过阈值40cm,溢水风险',
-    ownerUnit: '水务集团', address: '滨江路化工园区南门', purpose: '化工区雨水排放',
-    status: 'completed', createTime: '2025-03-31 14:00:00', completeTime: '2025-04-01 09:30:00',
-    completionHours: 19.5, assignee: '李志强', satisfaction: 4,
-    approvalRemark: '排水作业完成,水位恢复正常',
-    feedback: {
-      workerName: '李志强', feedbackTime: '2025-04-01 08:00:00',
-      description: '使用抽水泵排水,清理管道堵塞物,水位已降至安全线以下',
-      images: ['https://fuss10.elemecdn.com/3/5e/4bbf4c1b1b4f1e1c0b4b4c1b1b4f1e1c.jpeg']
-    },
-    processLogs: [
-      { id: 1, time: '2025-03-31 14:00:00', content: '工单创建', type: 'info', operator: '系统' },
-      { id: 2, time: '2025-03-31 15:00:00', content: '指派李志强紧急处置', type: 'primary', operator: '调度中心' },
-      { id: 3, time: '2025-04-01 08:00:00', content: '提交反馈,申请审批', type: 'warning', operator: '李志强' },
-      { id: 4, time: '2025-04-01 09:30:00', content: '审批通过办结', type: 'success', operator: '审批人' }
-    ]
-  },
-  {
-    id: 3, code: 'WO-20250330-003', caseCode: 'CASE-20250330-003', alertLevel: 'warning',
-    alertType: '异常震动', alertDesc: '异常震动112mg,超过阈值80mg,疑似外力冲击',
-    ownerUnit: '市政设施管理处', address: '古城街北段树下', purpose: '老城区排水',
-    status: 'completed', createTime: '2025-03-30 10:15:00', completeTime: '2025-03-30 17:00:00',
-    completionHours: 6.75, assignee: '王大明', satisfaction: 5,
-    approvalRemark: '现场查看为施工影响,已协调施工方注意保护',
-    feedback: {
-      workerName: '王大明', feedbackTime: '2025-03-30 16:00:00',
-      description: '附近有道路施工,大型车辆碾压导致震动,已设置警示标志',
-      images: ['https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg']
-    },
-    processLogs: [
-      { id: 1, time: '2025-03-30 10:15:00', content: '工单创建', type: 'info', operator: '系统' },
-      { id: 2, time: '2025-03-30 11:00:00', content: '指派王大明现场核实', type: 'primary', operator: '调度中心' },
-      { id: 3, time: '2025-03-30 16:00:00', content: '反馈处置结果', type: 'warning', operator: '王大明' },
-      { id: 4, time: '2025-03-30 17:00:00', content: '审批办结', type: 'success', operator: '审批人' }
-    ]
-  },
-  {
-    id: 4, code: 'WO-20250329-004', caseCode: 'CASE-20250329-004', alertLevel: 'warning',
-    alertType: '井盖开启异常', alertDesc: '井盖非正常开启',
-    ownerUnit: '市政工程管理处', address: '南港路旧车站段', purpose: '市政井盖',
-    status: 'completed', createTime: '2025-03-29 08:20:00', completeTime: '2025-03-29 11:30:00',
-    completionHours: 3.17, assignee: '陈辉', satisfaction: 4,
-    approvalRemark: '误报警,已复位',
-    feedback: {
-      workerName: '陈辉', feedbackTime: '2025-03-29 10:30:00',
-      description: '经核实为附近小孩玩耍误开井盖,已关闭并加装锁扣',
-      images: []
-    },
-    processLogs: [
-      { id: 1, time: '2025-03-29 08:20:00', content: '工单创建', type: 'info', operator: '系统' },
-      { id: 2, time: '2025-03-29 09:00:00', content: '指派陈辉前往', type: 'primary', operator: '调度中心' },
-      { id: 3, time: '2025-03-29 10:30:00', content: '反馈处置', type: 'warning', operator: '陈辉' },
-      { id: 4, time: '2025-03-29 11:30:00', content: '审批通过', type: 'success', operator: '审批人' }
-    ]
-  },
-  {
-    id: 5, code: 'WO-20250328-005', caseCode: 'CASE-20250328-005', alertLevel: 'critical',
-    alertType: '气体超标', alertDesc: '井下H₂S浓度28ppm,超过阈值15ppm',
-    ownerUnit: '生态环境局', address: '石化大道南侧', purpose: '化工区排污',
-    status: 'completed', createTime: '2025-03-28 13:00:00', completeTime: '2025-03-29 10:00:00',
-    completionHours: 21, assignee: '赵志远', satisfaction: 5,
-    approvalRemark: '气体浓度已下降至安全范围,持续监测中',
-    feedback: {
-      workerName: '赵志远', feedbackTime: '2025-03-29 09:00:00',
-      description: '检查井内通风,气体已消散,未发现泄漏源',
-      images: ['https://fuss10.elemecdn.com/3/5e/4bbf4c1b1b4f1e1c0b4b4c1b1b4f1e1c.jpeg']
-    },
-    processLogs: [
-      { id: 1, time: '2025-03-28 13:00:00', content: '工单创建', type: 'info', operator: '系统' },
-      { id: 2, time: '2025-03-28 14:00:00', content: '指派赵志远处置', type: 'primary', operator: '调度中心' },
-      { id: 3, time: '2025-03-29 09:00:00', content: '反馈处置', type: 'warning', operator: '赵志远' },
-      { id: 4, time: '2025-03-29 10:00:00', content: '审批办结', type: 'success', operator: '审批人' }
-    ]
-  },
-  {
-    id: 6, code: 'WO-20250327-006', caseCode: 'CASE-20250327-006', alertLevel: 'warning',
-    alertType: '电池低电量', alertDesc: '设备电池电量仅剩12%,需及时更换',
-    ownerUnit: '市政设施管理处', address: '中央公园北门', purpose: '公园排水',
-    status: 'completed', createTime: '2025-03-27 09:00:00', completeTime: '2025-03-27 15:30:00',
-    completionHours: 6.5, assignee: '张建国', satisfaction: 4,
-    approvalRemark: '电池已更换,设备恢复',
-    feedback: {
-      workerName: '张建国', feedbackTime: '2025-03-27 14:30:00',
-      description: '已更换新电池,设备电压恢复正常',
-      images: []
-    },
-    processLogs: [
-      { id: 1, time: '2025-03-27 09:00:00', content: '工单创建', type: 'info', operator: '系统' },
-      { id: 2, time: '2025-03-27 10:00:00', content: '指派张建国', type: 'primary', operator: '调度中心' },
-      { id: 3, time: '2025-03-27 14:30:00', content: '反馈并提交审批', type: 'warning', operator: '张建国' },
-      { id: 4, time: '2025-03-27 15:30:00', content: '审批通过', type: 'success', operator: '审批人' }
-    ]
-  }
-])
+import {computed, onMounted, ref} from 'vue'
+import {CircleCheck, Refresh, Search} from '@element-plus/icons-vue'
+import {ElMessage} from 'element-plus'
+import {getManholeWorkOrderDetail, getManholeWorkOrderFinishedPage} from '@/api/pipeNetwork/basic'
 
-// 筛选条件
+const orders = ref([])
+const loading = ref(false)
+const detailLoading = ref(false)
 const searchKeyword = ref('')
-const alertLevelFilter = ref('')
+const orderLevelFilter = ref('')
 const dateRange = ref([])
-const timeFilter = ref('all')  // week, month, all
+const timeFilter = ref('all')
 const currentPage = ref(1)
 const pageSize = ref(10)
-const loading = ref(false)
-
-// 弹窗/抽屉状态
 const detailDrawerVisible = ref(false)
-const feedbackDialogVisible = ref(false)
 const currentOrder = ref(null)
-const currentFeedback = ref(null)
 
-// 计算属性 - 基础筛选
-const filteredOrders = computed(() => {
-  let list = [...mockCompletedOrders.value]
-
-  // 报警级别筛选
-  if (alertLevelFilter.value) {
-    list = list.filter(o => o.alertLevel === alertLevelFilter.value)
-  }
-
-  // 关键词搜索
-  if (searchKeyword.value) {
-    const kw = searchKeyword.value.toLowerCase()
-    list = list.filter(o =>
-        o.code.toLowerCase().includes(kw) ||
-        o.caseCode.toLowerCase().includes(kw) ||
-        o.address.toLowerCase().includes(kw) ||
-        o.assignee.toLowerCase().includes(kw)
-    )
-  }
-
-  // 日期范围筛选
-  if (dateRange.value && dateRange.value.length === 2) {
-    const start = new Date(dateRange.value[0])
-    const end = new Date(dateRange.value[1])
-    list = list.filter(o => {
-      const completeDate = new Date(o.completeTime)
-      return completeDate >= start && completeDate <= end
-    })
-  }
-
-  // 时间筛选(本周/本月)
-  const now = new Date()
-  if (timeFilter.value === 'week') {
-    const weekAgo = new Date(now.setDate(now.getDate() - 7))
-    list = list.filter(o => new Date(o.completeTime) >= weekAgo)
-  } else if (timeFilter.value === 'month') {
-    const monthAgo = new Date(now.setMonth(now.getMonth() - 1))
-    list = list.filter(o => new Date(o.completeTime) >= monthAgo)
-  }
-
-  return list
-})
-
-const paginatedOrders = computed(() => {
-  const start = (currentPage.value - 1) * pageSize.value
-  return filteredOrders.value.slice(start, start + pageSize.value)
-})
-
-// 统计数据
-const completedOrdersCount = computed(() => mockCompletedOrders.value.length)
-const weeklyCompleted = computed(() => {
-  const weekAgo = new Date()
-  weekAgo.setDate(weekAgo.getDate() - 7)
-  return mockCompletedOrders.value.filter(o => new Date(o.completeTime) >= weekAgo).length
-})
-const monthlyCompleted = computed(() => {
-  const monthAgo = new Date()
-  monthAgo.setMonth(monthAgo.getMonth() - 1)
-  return mockCompletedOrders.value.filter(o => new Date(o.completeTime) >= monthAgo).length
-})
-const avgCompletionHours = computed(() => {
-  const sum = mockCompletedOrders.value.reduce((acc, o) => acc + o.completionHours, 0)
-  return (sum / mockCompletedOrders.value.length).toFixed(1)
-})
-
-// 方法
-const setTimeFilter = (filter) => {
-  timeFilter.value = filter
-  currentPage.value = 1
-}
-
-const refreshList = () => {
-  loading.value = true
-  setTimeout(() => {
-    loading.value = false
-    ElMessage.success('列表已刷新')
-  }, 500)
+function formatDateTime(value) {
+  if (!value) return '—'
+  return String(value).replace('T', ' ')
 }
 
-const exportData = () => {
-  ElMessage.success('导出工单报表(演示)')
-}
-
-const handleRowClick = (row) => {
-  viewDetail(row)
-}
-
-const viewDetail = (order) => {
-  currentOrder.value = order
-  detailDrawerVisible.value = true
-}
-
-const viewFeedback = (order) => {
-  if (order.feedback) {
-    currentFeedback.value = order.feedback
-    feedbackDialogVisible.value = true
-  } else {
-    ElMessage.info('暂无反馈详情')
+function getOrderLevelMeta(level) {
+  const map = {
+    1: { text: '紧急', tag: 'danger' },
+    2: { text: '一般', tag: 'warning' },
+    3: { text: '低', tag: 'info' }
   }
+  return map[Number(level)] || { text: '—', tag: 'info' }
 }
 
-const viewCaseDetail = () => {
-  ElMessage.info(`查看案件 ${currentOrder.value?.caseCode} 详情(演示)`)
-}
-</script>
-
-<style scoped>
-.completed-orders {
-  background: #f0f2f6;
-  min-height: 100vh;
-  padding: 20px;
-}
-
-.page-header {
-  background: white;
-  border-radius: 16px;
-  padding: 16px 24px;
-  margin-bottom: 20px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 12px;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.04);
-}
-
-.header-left {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-
-.header-left .title {
-  font-size: 20px;
-  font-weight: 600;
-  color: #1f2d3d;
-}
-
-.header-right {
-  display: flex;
-  gap: 12px;
-  align-items: center;
-  flex-wrap: wrap;
-}
-
-.stats-cards {
-  display: flex;
-  gap: 16px;
-  margin-bottom: 20px;
-  flex-wrap: wrap;
-}
-
-.stat-card {
-  background: white;
-  border-radius: 12px;
-  padding: 12px 24px;
-  text-align: center;
-  cursor: pointer;
-  transition: all 0.2s;
-  min-width: 110px;
-  box-shadow: 0 1px 4px rgba(0,0,0,0.05);
+function getOrderTypeText(type) {
+  const map = {
+    1: '故障维修',
+    2: '日常巡检',
+    3: '设备保养'
+  }
+  return map[Number(type)] || '—'
 }
 
-.stat-card:hover {
-  transform: translateY(-2px);
-  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
+function getOrderStatusMeta(status) {
+  const map = {
+    6: { text: '已结案', tag: 'success' },
+    7: { text: '已驳回', tag: 'danger' },
+    9: { text: '已延期', tag: 'warning' }
+  }
+  return map[Number(status)] || { text: '—', tag: 'info' }
 }
 
-.stat-card.active {
-  border: 2px solid #67c23a;
-  background: #f0f9f0;
+function getOnlineStatusText(status) {
+  const map = {
+    0: '离线',
+    1: '在线'
+  }
+  return map[Number(status)] || '—'
 }
 
-.stat-value {
-  font-size: 28px;
-  font-weight: bold;
-  color: #2c3e50;
+function getAlarmStatusText(status) {
+  const map = {
+    0: '正常',
+    1: '报警'
+  }
+  return map[Number(status)] || '—'
 }
 
-.stat-label {
-  font-size: 13px;
-  color: #606266;
-  margin-top: 4px;
+function getCurrentStatusText(status) {
+  const map = {
+    1: '在用',
+    2: '闲置',
+    3: '维修',
+    4: '报废',
+    5: '待入库'
+  }
+  return map[Number(status)] || '—'
 }
 
-.order-table-container {
-  background: white;
-  border-radius: 16px;
-  padding: 16px;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.04);
+function normalizeOrder(item) {
+  const levelMeta = getOrderLevelMeta(item.orderLevel)
+  const statusMeta = getOrderStatusMeta(item.orderStatus)
+  return {
+    ...item,
+    orderLevelText: levelMeta.text,
+    orderLevelTag: levelMeta.tag,
+    orderTypeText: getOrderTypeText(item.orderType),
+    orderStatusText: statusMeta.text,
+    orderStatusTag: statusMeta.tag,
+    createTimeText: formatDateTime(item.createTime),
+    dispatchTimeText: formatDateTime(item.dispatchTime),
+    planFinishTimeText: formatDateTime(item.planFinishTime),
+    delayedDeadlineText: formatDateTime(item.delayedDeadline),
+    verifyTimeText: formatDateTime(item.verifyTime),
+    realFinishTimeText: formatDateTime(item.realFinishTime),
+    updateTimeText: formatDateTime(item.updateTime),
+    currentStatusText: getCurrentStatusText(item.equipmentStatus?.currentStatus),
+    onlineStatusText: getOnlineStatusText(item.equipmentStatus?.onlineStatus),
+    alarmStatusText: getAlarmStatusText(item.equipmentStatus?.alarmStatus),
+    statusUpdateTimeText: formatDateTime(item.equipmentStatus?.statusUpdateTime)
+  }
 }
 
-.pagination-wrapper {
-  display: flex;
-  justify-content: flex-end;
-  margin-top: 16px;
-  padding-top: 12px;
-  border-top: 1px solid #e9ecef;
+function isWithinRecentDays(value, days) {
+  if (!value) return false
+  const time = new Date(value).getTime()
+  if (Number.isNaN(time)) return false
+  return Date.now() - time <= days * 24 * 3600000
 }
 
-.order-detail {
-  padding: 8px;
-}
+const filteredOrders = computed(() => {
+  if (timeFilter.value === 'week') {
+    return orders.value.filter(item => isWithinRecentDays(item.realFinishTime, 7))
+  }
+  if (timeFilter.value === 'month') {
+    return orders.value.filter(item => isWithinRecentDays(item.realFinishTime, 30))
+  }
+  return orders.value
+})
 
-.sub-title {
-  font-weight: 600;
-  margin: 20px 0 12px 0;
-  font-size: 15px;
-  color: #2c3e50;
-  display: flex;
-  align-items: center;
-  gap: 8px;
-}
+const paginatedOrders = computed(() => {
+  const start = (currentPage.value - 1) * pageSize.value
+  return filteredOrders.value.slice(start, start + pageSize.value)
+})
 
-.feedback-images {
-  display: flex;
-  gap: 12px;
-  flex-wrap: wrap;
-  margin-top: 8px;
-}
+const completedOrdersCount = computed(() => orders.value.length)
+const weeklyCompleted = computed(() => orders.value.filter(item => isWithinRecentDays(item.realFinishTime, 7)).length)
+const monthlyCompleted = computed(() => orders.value.filter(item => isWithinRecentDays(item.realFinishTime, 30)).length)
 
-.feedback-img {
-  width: 100px;
-  height: 80px;
-  border-radius: 8px;
-  object-fit: cover;
-  cursor: pointer;
+async function loadOrders() {
+  loading.value = true
+  try {
+    const keyword = searchKeyword.value.trim()
+    const payload = {
+      pageNum: 1,
+      pageSize: 200,
+      orderLevel: orderLevelFilter.value || undefined,
+      orderNo: keyword || undefined,
+      caseNo: keyword || undefined,
+      startTime: dateRange.value?.[0] ? `${dateRange.value[0]} 00:00:00` : undefined,
+      endTime: dateRange.value?.[1] ? `${dateRange.value[1]} 23:59:59` : undefined
+    }
+    const res = await getManholeWorkOrderFinishedPage(payload)
+    const records = Array.isArray(res?.data?.records) ? res.data.records : []
+    orders.value = records.map(normalizeOrder)
+    currentPage.value = 1
+    if (currentOrder.value) {
+      const matched = orders.value.find(item => item.orderId === currentOrder.value.orderId)
+      if (matched) {
+        await viewDetail(matched)
+      } else {
+        currentOrder.value = null
+        detailDrawerVisible.value = false
+      }
+    }
+  } catch (error) {
+    orders.value = []
+    currentOrder.value = null
+    ElMessage.error('办结工单加载失败')
+  } finally {
+    loading.value = false
+  }
 }
 
-.process-logs {
-  margin-top: 16px;
+async function viewDetail(order) {
+  detailLoading.value = true
+  detailDrawerVisible.value = true
+  try {
+    const res = await getManholeWorkOrderDetail(order.orderId)
+    currentOrder.value = normalizeOrder(res?.data || order)
+  } catch (error) {
+    currentOrder.value = order
+    ElMessage.error('工单详情加载失败')
+  } finally {
+    detailLoading.value = false
+  }
 }
 
-.operator {
-  font-size: 12px;
-  color: #909399;
-  margin-left: 12px;
+function handleRowClick(row) {
+  viewDetail(row)
 }
 
-:deep(.el-table__row) {
-  cursor: pointer;
+function setTimeFilter(filter) {
+  timeFilter.value = filter
+  currentPage.value = 1
 }
 
-:deep(.el-table__row:hover) {
-  background-color: #f5f7fa;
+function getLogType(operType) {
+  const map = {
+    2: 'primary',
+    3: 'warning',
+    4: 'primary',
+    5: 'warning',
+    6: 'success',
+    7: 'danger',
+    8: 'warning',
+    9: 'success'
+  }
+  return map[Number(operType)] || 'info'
 }
 
-:deep(.el-rate) {
-  display: inline-flex;
-}
-</style>
+onMounted(() => {
+  loadOrders()
+})
+</script>

+ 407 - 386
src/views/subSystem/manholeCover/caseHandling/handlingWorkO.vue

@@ -1,281 +1,338 @@
 <!--经办工单-->
 <template>
-  <div class="pending-orders">
-    <!-- 页面头部 -->
-    <div class="page-header">
-      <div class="header-left">
+  <div class="min-h-screen bg-[#f0f2f6] p-5">
+    <div class="mb-5 flex flex-wrap items-center justify-between gap-3 rounded-2xl bg-white px-6 py-4 shadow-[0_2px_8px_rgba(0,0,0,0.04)]">
+      <div class="flex items-center gap-3">
         <el-icon><List /></el-icon>
-        <span class="title">待处理工单</span>
+        <span class="text-[20px] font-600 text-[#1f2d3d]">经办工单</span>
         <el-tag type="danger" effect="dark" v-if="pendingOrdersCount > 0">待处理 {{ pendingOrdersCount }}</el-tag>
       </div>
-      <div class="header-right">
+      <div class="flex flex-wrap items-center gap-3">
         <el-input
-            v-model="searchKeyword"
-            placeholder="搜索工单编号/案件编号/地址"
-            clearable
-            :prefix-icon="Search"
-            style="width: 260px"
-            size="default"
+          v-model="searchKeyword"
+          placeholder="搜索工单编号/案件编号"
+          clearable
+          :prefix-icon="Search"
+          class="w-[260px]"
+          @keyup.enter="loadOrders"
         />
-        <el-select v-model="alertLevelFilter" placeholder="报警级别" clearable style="width: 120px" size="default">
-          <el-option label="严重" value="critical" />
-          <el-option label="一般" value="warning" />
+        <el-select v-model="orderLevelFilter" placeholder="优先级" clearable class="w-[120px]">
+          <el-option label="紧急" :value="1" />
+          <el-option label="一般" :value="2" />
+          <el-option label="低" :value="3" />
         </el-select>
-        <el-button type="primary" :icon="Refresh" @click="refreshList">刷新</el-button>
+        <el-button type="primary" :icon="Refresh" @click="loadOrders">刷新</el-button>
       </div>
     </div>
 
-    <!-- 统计卡片 -->
-    <div class="stats-cards">
-      <div class="stat-card" :class="{ active: statusFilter === '' }" @click="setStatusFilter('')">
-        <div class="stat-value">{{ totalOrders }}</div>
-        <div class="stat-label">全部工单</div>
+    <div class="mb-5 flex flex-wrap gap-4">
+      <div
+        class="min-w-[100px] cursor-pointer rounded-xl bg-white px-6 py-3 text-center shadow-[0_1px_4px_rgba(0,0,0,0.05)] transition-all duration-200 hover:-translate-y-[2px] hover:shadow-[0_4px_12px_rgba(0,0,0,0.1)]"
+        :class="statusFilter === '' ? 'border-2 border-[#409eff] bg-[#ecf5ff]' : 'border-2 border-transparent'"
+        @click="setStatusFilter('')"
+      >
+        <div class="text-[28px] font-bold">{{ totalOrders }}</div>
+        <div class="mt-1 text-[13px] text-[#606266]">全部工单</div>
       </div>
-      <div class="stat-card pending" :class="{ active: statusFilter === 'pending' }" @click="setStatusFilter('pending')">
-        <div class="stat-value">{{ pendingOrdersCount }}</div>
-        <div class="stat-label">待处理</div>
+      <div
+        class="min-w-[100px] cursor-pointer rounded-xl bg-white px-6 py-3 text-center shadow-[0_1px_4px_rgba(0,0,0,0.05)] transition-all duration-200 hover:-translate-y-[2px] hover:shadow-[0_4px_12px_rgba(0,0,0,0.1)]"
+        :class="statusFilter === 'pending' ? 'border-2 border-[#409eff] bg-[#ecf5ff]' : 'border-2 border-transparent'"
+        @click="setStatusFilter('pending')"
+      >
+        <div class="text-[28px] font-bold text-[#f56c6c]">{{ pendingOrdersCount }}</div>
+        <div class="mt-1 text-[13px] text-[#606266]">待处理</div>
       </div>
-      <div class="stat-card processing" :class="{ active: statusFilter === 'processing' }" @click="setStatusFilter('processing')">
-        <div class="stat-value">{{ processingOrdersCount }}</div>
-        <div class="stat-label">处理中</div>
+      <div
+        class="min-w-[100px] cursor-pointer rounded-xl bg-white px-6 py-3 text-center shadow-[0_1px_4px_rgba(0,0,0,0.05)] transition-all duration-200 hover:-translate-y-[2px] hover:shadow-[0_4px_12px_rgba(0,0,0,0.1)]"
+        :class="statusFilter === 'processing' ? 'border-2 border-[#409eff] bg-[#ecf5ff]' : 'border-2 border-transparent'"
+        @click="setStatusFilter('processing')"
+      >
+        <div class="text-[28px] font-bold text-[#e6a23c]">{{ processingOrdersCount }}</div>
+        <div class="mt-1 text-[13px] text-[#606266]">处理中</div>
       </div>
-      <div class="stat-card overdue" :class="{ active: statusFilter === 'overdue' }" @click="setStatusFilter('overdue')">
-        <div class="stat-value">{{ overdueCount }}</div>
-        <div class="stat-label">已逾期</div>
+      <div
+        class="min-w-[100px] cursor-pointer rounded-xl bg-white px-6 py-3 text-center shadow-[0_1px_4px_rgba(0,0,0,0.05)] transition-all duration-200 hover:-translate-y-[2px] hover:shadow-[0_4px_12px_rgba(0,0,0,0.1)]"
+        :class="statusFilter === 'overdue' ? 'border-2 border-[#409eff] bg-[#ecf5ff]' : 'border-2 border-transparent'"
+        @click="setStatusFilter('overdue')"
+      >
+        <div class="text-[28px] font-bold text-[#f56c6c]">{{ overdueCount }}</div>
+        <div class="mt-1 text-[13px] text-[#606266]">已逾期</div>
       </div>
     </div>
 
-    <!-- 工单列表 -->
-    <div class="order-table-container">
-      <el-table
-          :data="paginatedOrders"
-          stripe
-          border
-          style="width: 100%"
-          v-loading="loading"
-          @row-click="handleRowClick"
-      >
+    <div class="rounded-2xl bg-white p-4 shadow-[0_2px_8px_rgba(0,0,0,0.04)]">
+      <el-table :data="paginatedOrders" stripe border class="w-full" row-class-name="cursor-pointer" v-loading="loading" @row-click="handleRowClick">
         <el-table-column type="index" label="序号" width="55" align="center" />
-        <el-table-column prop="code" label="工单编号" width="160" sortable />
-        <el-table-column prop="caseCode" label="关联案件编号" width="160" />
-        <el-table-column prop="alertLevel" label="报警级别" width="100" align="center">
+        <el-table-column prop="orderNo" label="工单编号" min-width="170" />
+        <el-table-column prop="caseNo" label="关联案件编号" min-width="170" />
+        <el-table-column prop="orderLevelText" label="优先级" width="100" align="center">
           <template #default="{ row }">
-            <el-tag :type="row.alertLevel === 'critical' ? 'danger' : 'warning'" size="small">
-              {{ row.alertLevel === 'critical' ? '严重' : '一般' }}
-            </el-tag>
+            <el-tag :type="row.orderLevelTag" size="small">{{ row.orderLevelText }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="alertType" label="报警类型" width="120" />
-        <el-table-column prop="alertDesc" label="报警描述" min-width="200" show-overflow-tooltip />
-        <el-table-column prop="ownerUnit" 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">
+        <el-table-column prop="orderTypeText" label="工单类型" width="110" />
+        <el-table-column prop="orderDesc" label="问题描述" min-width="220" show-overflow-tooltip />
+        <el-table-column prop="deviceCode" label="设备编号" width="150" />
+        <el-table-column prop="orderStatusText" label="工单状态" width="110" align="center">
           <template #default="{ row }">
-            <el-tag :type="getStatusType(row.status)" size="small">
-              {{ getStatusText(row.status) }}
-            </el-tag>
+            <el-tag :type="row.orderStatusTag" size="small">{{ row.orderStatusText }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="createTime" label="创建时间" width="160" sortable />
-        <el-table-column label="操作" width="120" fixed="right" align="center">
+        <el-table-column prop="createTimeText" label="创建时间" width="170" />
+        <el-table-column prop="planFinishTimeText" label="计划完成时间" width="170" />
+        <el-table-column label="操作" width="150" fixed="right" align="center">
           <template #default="{ row }">
             <el-button link type="primary" size="small" @click.stop="viewDetail(row)">查看详情</el-button>
-            <el-button
-                v-if="row.status === 'pending'"
-                link type="success"
-                size="small"
-                @click.stop="handleOrder(row)"
-            >处理</el-button>
+            <el-button v-if="canAcceptOrder(row)" link type="warning" size="small" @click.stop="acceptOrder(row)">接单</el-button>
+            <el-button v-else-if="canStartRepair(row)" link type="success" size="small" @click.stop="startRepair(row)">开始维修</el-button>
+            <el-button v-else-if="canSubmitVerify(row)" link type="warning" size="small" @click.stop="submitVerify(row)">提交验收</el-button>
           </template>
         </el-table-column>
       </el-table>
 
-      <!-- 分页 -->
-      <div class="pagination-wrapper">
+      <div class="mt-4 flex justify-end border-t border-[#e9ecef] pt-3">
         <el-pagination
-            background
-            layout="total, sizes, prev, pager, next, jumper"
-            :total="filteredOrders.length"
-            v-model:current-page="currentPage"
-            v-model:page-size="pageSize"
-            :page-sizes="[10, 20, 50, 100]"
+          background
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="filteredOrders.length"
+          v-model:current-page="currentPage"
+          v-model:page-size="pageSize"
+          :page-sizes="[10, 20, 50]"
         />
       </div>
     </div>
 
-    <!-- 工单详情抽屉 -->
-    <el-drawer v-model="detailDrawerVisible" :title="`工单详情 - ${currentOrder?.code}`" size="500px" direction="rtl">
-      <div v-if="currentOrder" class="order-detail">
+    <el-drawer v-model="detailDrawerVisible" :title="`工单详情 - ${currentOrder?.orderNo || ''}`" size="540px" direction="rtl">
+      <div v-if="currentOrder" class="p-2" v-loading="detailLoading">
         <el-descriptions :column="1" border>
-          <el-descriptions-item label="工单编号">{{ currentOrder.code }}</el-descriptions-item>
-          <el-descriptions-item label="关联案件">
-            <el-link type="primary" @click="viewCaseDetail">{{ currentOrder.caseCode }}</el-link>
-          </el-descriptions-item>
-          <el-descriptions-item label="报警级别">
-            <el-tag :type="currentOrder.alertLevel === 'critical' ? 'danger' : 'warning'">
-              {{ currentOrder.alertLevel === 'critical' ? '严重' : '一般' }}
-            </el-tag>
-          </el-descriptions-item>
-          <el-descriptions-item label="报警类型">{{ currentOrder.alertType }}</el-descriptions-item>
-          <el-descriptions-item label="报警描述">{{ currentOrder.alertDesc }}</el-descriptions-item>
-          <el-descriptions-item label="权属单位">{{ currentOrder.ownerUnit }}</el-descriptions-item>
-          <el-descriptions-item label="详细地址">{{ currentOrder.address }}</el-descriptions-item>
-          <el-descriptions-item label="用途">{{ currentOrder.purpose || '市政排水' }}</el-descriptions-item>
+          <el-descriptions-item label="工单ID">{{ currentOrder.orderId || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="工单编号">{{ currentOrder.orderNo }}</el-descriptions-item>
+          <el-descriptions-item label="报警ID">{{ currentOrder.alarmId || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="案件ID">{{ currentOrder.caseId || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="关联案件编号">{{ currentOrder.caseNo || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="设备ID">{{ currentOrder.deviceId || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="设备编号">{{ currentOrder.deviceCode || '—' }}</el-descriptions-item>
           <el-descriptions-item label="工单状态">
-            <el-tag :type="getStatusType(currentOrder.status)">{{ getStatusText(currentOrder.status) }}</el-tag>
+            <el-tag :type="currentOrder.orderStatusTag">{{ currentOrder.orderStatusText }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="优先级">
+            <el-tag :type="currentOrder.orderLevelTag">{{ currentOrder.orderLevelText }}</el-tag>
           </el-descriptions-item>
-          <el-descriptions-item label="创建时间">{{ currentOrder.createTime }}</el-descriptions-item>
-          <el-descriptions-item label="指派人">{{ currentOrder.assignee || '未指派' }}</el-descriptions-item>
-          <el-descriptions-item label="计划完成时间">{{ currentOrder.deadline || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="工单类型">{{ currentOrder.orderTypeText }}</el-descriptions-item>
+          <el-descriptions-item label="问题描述">{{ currentOrder.orderDesc || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="负责部门ID">{{ currentOrder.deptId || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="设备类别">{{ currentOrder.equipmentTypeName || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="接单人ID">{{ currentOrder.receiveUser || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="派单人ID">{{ currentOrder.dispatchUser || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="派单时间">{{ currentOrder.dispatchTimeText }}</el-descriptions-item>
+          <el-descriptions-item label="创建时间">{{ currentOrder.createTimeText }}</el-descriptions-item>
+          <el-descriptions-item label="计划完成时间">{{ currentOrder.planFinishTimeText }}</el-descriptions-item>
+          <el-descriptions-item label="延期原因">{{ currentOrder.delayedReason || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="延期截止时间">{{ currentOrder.delayedDeadlineText }}</el-descriptions-item>
+          <el-descriptions-item label="验收人ID">{{ currentOrder.verifyUser || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="验收时间">{{ currentOrder.verifyTimeText }}</el-descriptions-item>
+          <el-descriptions-item label="实际完成时间">{{ currentOrder.realFinishTimeText }}</el-descriptions-item>
+          <el-descriptions-item label="维修处置结果">{{ currentOrder.repairResult || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="设备运维状态">{{ currentOrder.currentStatusText }}</el-descriptions-item>
+          <el-descriptions-item label="设备在线状态">{{ currentOrder.onlineStatusText }}</el-descriptions-item>
+          <el-descriptions-item label="设备报警状态">{{ currentOrder.alarmStatusText }}</el-descriptions-item>
+          <el-descriptions-item label="状态更新时间">{{ currentOrder.statusUpdateTimeText }}</el-descriptions-item>
+          <el-descriptions-item label="更新时间">{{ currentOrder.updateTimeText }}</el-descriptions-item>
         </el-descriptions>
 
-        <!-- 处理记录 -->
-        <div class="process-logs" v-if="currentOrder.processLogs?.length">
-          <div class="sub-title">处理记录</div>
-          <el-timeline>
-            <el-timeline-item v-for="log in currentOrder.processLogs" :key="log.id" :timestamp="log.time" :type="log.type">
-              {{ log.content }}
+        <div class="mt-4">
+          <div class="mb-3 mt-4 text-[14px] font-600 text-[#2c3e50]">处理记录</div>
+          <el-timeline v-if="currentOrder.workOrderLogList?.length">
+            <el-timeline-item
+              v-for="log in currentOrder.workOrderLogList"
+              :key="log.logId"
+              :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>
             </el-timeline-item>
           </el-timeline>
+          <el-empty v-else description="暂无处理记录" :image-size="80" />
         </div>
 
-        <!-- 操作按钮 -->
-        <div class="detail-actions" v-if="currentOrder.status === 'pending'">
-          <el-button type="primary" @click="handleOrder(currentOrder)">开始处理</el-button>
-          <el-button type="warning" @click="openAssignDialog">指派人员</el-button>
+        <div class="mt-6 flex justify-end gap-3">
+          <el-button v-if="canAcceptOrder(currentOrder)" type="warning" @click="acceptOrder(currentOrder)">接单</el-button>
+          <el-button v-if="canStartRepair(currentOrder)" type="primary" @click="startRepair(currentOrder)">开始维修</el-button>
+          <el-button v-if="canSubmitVerify(currentOrder)" type="success" @click="submitVerify(currentOrder)">提交验收</el-button>
+          <el-button
+            v-if="canDelay(currentOrder)"
+            type="warning"
+            plain
+            @click="openDelayDialog(currentOrder)"
+          >
+            申请延期
+          </el-button>
         </div>
+        <el-alert
+          v-if="!hasOperationButtons(currentOrder)"
+          class="mt-4"
+          title="当前状态暂无可操作项"
+          type="info"
+          :closable="false"
+          show-icon
+        />
       </div>
     </el-drawer>
 
-    <!-- 指派人员对话框 -->
-    <el-dialog v-model="assignDialogVisible" title="指派维护人员" width="400px">
-      <el-form>
-        <el-form-item label="选择维护人员">
-          <el-select v-model="selectedWorkerId" placeholder="请选择" style="width: 100%">
-            <el-option v-for="worker in workers" :key="worker.id" :label="worker.name" :value="worker.id" />
-          </el-select>
+    <el-dialog v-model="delayDialogVisible" title="申请延期" width="420px">
+      <el-form label-width="88px">
+        <el-form-item label="截止时间">
+          <el-date-picker
+            v-model="delayDeadline"
+            type="datetime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            placeholder="请选择延期截止时间"
+            class="w-full"
+          />
         </el-form-item>
-        <el-form-item label="备注">
-          <el-input type="textarea" v-model="assignRemark" placeholder="可选,填写派单说明" rows="2" />
+        <el-form-item label="延期原因">
+          <el-input v-model="delayReason" type="textarea" rows="3" placeholder="请输入延期原因" />
         </el-form-item>
       </el-form>
       <template #footer>
-        <el-button @click="assignDialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="confirmAssign">确认指派</el-button>
+        <el-button @click="delayDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitDelay">提交</el-button>
       </template>
     </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
-import { List, Search, Refresh } from '@element-plus/icons-vue'
-import { ElMessage } from 'element-plus'
-
-// 模拟工单数据
-const mockOrders = ref([
-  {
-    id: 1, code: 'WO-20250402-001', caseCode: 'CASE-20250402-001', alertLevel: 'critical',
-    alertType: '倾斜超标', alertDesc: '井盖倾斜角度14.2°,超过阈值8°,有跌落风险',
-    ownerUnit: '市政工程管理处', address: '人民路与解放路口东50m', purpose: '主干道排水',
-    status: 'pending', createTime: '2025-04-02 14:25:00', assignee: null, deadline: '2025-04-03 14:25:00',
-    processLogs: [{ id: 1, time: '2025-04-02 14:25:00', content: '工单自动创建', type: 'info' }]
-  },
-  {
-    id: 2, code: 'WO-20250402-002', caseCode: 'CASE-20250402-002', alertLevel: 'critical',
-    alertType: '水位超限', alertDesc: '井下水位62cm,超过阈值40cm,溢水风险',
-    ownerUnit: '水务集团', address: '滨江路化工园区南门', purpose: '化工区雨水排放',
-    status: 'processing', createTime: '2025-04-02 14:00:00', assignee: '张建国', deadline: '2025-04-03 10:00:00',
-    processLogs: [
-      { id: 1, time: '2025-04-02 14:00:00', content: '工单自动创建', type: 'info' },
-      { id: 2, time: '2025-04-02 14:30:00', content: '已指派张建国前往处置', type: 'primary' }
-    ]
-  },
-  {
-    id: 3, code: 'WO-20250402-003', caseCode: 'CASE-20250402-003', alertLevel: 'warning',
-    alertType: '异常震动', alertDesc: '异常震动112mg,超过阈值80mg,疑似外力冲击',
-    ownerUnit: '市政设施管理处', address: '古城街北段树下', purpose: '老城区排水',
-    status: 'pending', createTime: '2025-04-02 12:15:00', assignee: null, deadline: '2025-04-03 12:15:00',
-    processLogs: [{ id: 1, time: '2025-04-02 12:15:00', content: '工单自动创建', type: 'info' }]
-  },
-  {
-    id: 4, code: 'WO-20250402-004', caseCode: 'CASE-20250402-004', alertLevel: 'warning',
-    alertType: '井盖开启异常', alertDesc: '井盖非正常开启',
-    ownerUnit: '市政工程管理处', address: '南港路旧车站段', purpose: '市政井盖',
-    status: 'overdue', createTime: '2025-04-01 10:30:00', assignee: '王大明', deadline: '2025-04-02 10:30:00',
-    processLogs: [
-      { id: 1, time: '2025-04-01 10:30:00', content: '工单创建', type: 'info' },
-      { id: 2, time: '2025-04-01 11:00:00', content: '指派王大明处置', type: 'primary' },
-      { id: 3, time: '2025-04-02 10:30:00', content: '工单已逾期,待处理', type: 'danger' }
-    ]
-  },
-  {
-    id: 5, code: 'WO-20250402-005', caseCode: 'CASE-20250402-005', alertLevel: 'critical',
-    alertType: '气体超标', alertDesc: '井下H₂S浓度28ppm,超过阈值15ppm',
-    ownerUnit: '生态环境局', address: '石化大道南侧', purpose: '化工区排污',
-    status: 'pending', createTime: '2025-04-02 09:20:00', assignee: null, deadline: '2025-04-03 09:20:00',
-    processLogs: [{ id: 1, time: '2025-04-02 09:20:00', content: '工单自动创建', type: 'info' }]
-  },
-  {
-    id: 6, code: 'WO-20250402-006', caseCode: 'CASE-20250402-006', alertLevel: 'warning',
-    alertType: '电池低电量', alertDesc: '设备电池电量仅剩12%,需及时更换',
-    ownerUnit: '市政设施管理处', address: '中央公园北门', purpose: '公园排水',
-    status: 'processing', createTime: '2025-04-02 08:45:00', assignee: '陈辉', deadline: '2025-04-03 08:45:00',
-    processLogs: [
-      { id: 1, time: '2025-04-02 08:45:00', content: '工单创建', type: 'info' },
-      { id: 2, time: '2025-04-02 09:30:00', content: '指派陈辉前往更换电池', type: 'primary' }
-    ]
-  }
-])
-
-// 维护人员列表
-const workers = ref([
-  { id: 'w1', name: '张建国 - 维修一组' },
-  { id: 'w2', name: '李志强 - 维修二组' },
-  { id: 'w3', name: '王大明 - 应急抢修队' },
-  { id: 'w4', name: '陈辉 - 巡检专员' },
-  { id: 'w5', name: '赵志远 - 抢修班' }
-])
-
-// 筛选状态
+import {computed, onMounted, ref} from 'vue'
+import {List, Refresh, Search} from '@element-plus/icons-vue'
+import {ElMessage} from 'element-plus'
+import {
+  getManholeWorkOrderDetail,
+  getManholeWorkOrderProcessingPage,
+  operManholeWorkOrder
+} from '@/api/pipeNetwork/basic'
+
+const DEFAULT_OPER_USER_ID = 1
+
+const orders = ref([])
+const loading = ref(false)
+const detailLoading = ref(false)
+const detailDrawerVisible = ref(false)
+const delayDialogVisible = ref(false)
+const currentOrder = ref(null)
 const searchKeyword = ref('')
-const alertLevelFilter = ref('')
-const statusFilter = ref('pending')  // 默认显示待处理
+const orderLevelFilter = ref('')
+const statusFilter = ref('')
 const currentPage = ref(1)
 const pageSize = ref(10)
-const loading = ref(false)
+const delayDeadline = ref('')
+const delayReason = ref('')
 
-// 弹窗/抽屉状态
-const detailDrawerVisible = ref(false)
-const assignDialogVisible = ref(false)
-const currentOrder = ref(null)
-const selectedWorkerId = ref('')
-const assignRemark = ref('')
+function formatDateTime(value) {
+  if (!value) return '—'
+  return String(value).replace('T', ' ')
+}
 
-// 计算属性
-const filteredOrders = computed(() => {
-  let list = mockOrders.value
+function getOrderLevelMeta(level) {
+  const map = {
+    1: { text: '紧急', tag: 'danger' },
+    2: { text: '一般', tag: 'warning' },
+    3: { text: '低', tag: 'info' }
+  }
+  return map[Number(level)] || { text: '—', tag: 'info' }
+}
 
-  // 状态筛选
-  if (statusFilter.value) {
-    list = list.filter(o => o.status === statusFilter.value)
+function getOrderTypeText(type) {
+  const map = {
+    1: '故障维修',
+    2: '日常巡检',
+    3: '设备保养'
   }
+  return map[Number(type)] || '—'
+}
 
-  // 报警级别筛选
-  if (alertLevelFilter.value) {
-    list = list.filter(o => o.alertLevel === alertLevelFilter.value)
+function getOrderStatusMeta(status) {
+  const map = {
+    1: { key: 'pending', text: '待派单', tag: 'danger' },
+    2: { key: 'pending', text: '待接单', tag: 'danger' },
+    3: { key: 'processing', text: '已接单', tag: 'warning' },
+    4: { key: 'processing', text: '处理中', tag: 'warning' },
+    5: { key: 'verify', text: '待验收', tag: 'primary' },
+    6: { key: 'completed', text: '已结案', tag: 'success' },
+    7: { key: 'completed', text: '已驳回', tag: 'danger' },
+    8: { key: 'verify', text: '延期审核', tag: 'warning' },
+    9: { key: 'completed', text: '已延期', tag: 'danger' }
   }
+  return map[Number(status)] || { key: '', text: '—', tag: 'info' }
+}
 
-  // 关键词搜索
-  if (searchKeyword.value) {
-    const kw = searchKeyword.value.toLowerCase()
-    list = list.filter(o =>
-        o.code.toLowerCase().includes(kw) ||
-        o.caseCode.toLowerCase().includes(kw) ||
-        o.address.toLowerCase().includes(kw)
-    )
+function getOnlineStatusText(status) {
+  const map = {
+    0: '离线',
+    1: '在线'
   }
+  return map[Number(status)] || '—'
+}
 
+function getAlarmStatusText(status) {
+  const map = {
+    0: '正常',
+    1: '报警'
+  }
+  return map[Number(status)] || '—'
+}
+
+function getCurrentStatusText(status) {
+  const map = {
+    1: '在用',
+    2: '闲置',
+    3: '维修',
+    4: '报废',
+    5: '待入库'
+  }
+  return map[Number(status)] || '—'
+}
+
+function normalizeOrder(item) {
+  const levelMeta = getOrderLevelMeta(item.orderLevel)
+  const statusMeta = getOrderStatusMeta(item.orderStatus)
+  return {
+    ...item,
+    orderLevelText: levelMeta.text,
+    orderLevelTag: levelMeta.tag,
+    orderTypeText: getOrderTypeText(item.orderType),
+    orderStatusKey: statusMeta.key,
+    orderStatusText: statusMeta.text,
+    orderStatusTag: statusMeta.tag,
+    createTimeText: formatDateTime(item.createTime),
+    dispatchTimeText: formatDateTime(item.dispatchTime),
+    planFinishTimeText: formatDateTime(item.planFinishTime),
+    delayedDeadlineText: formatDateTime(item.delayedDeadline),
+    realFinishTimeText: formatDateTime(item.realFinishTime),
+    verifyTimeText: formatDateTime(item.verifyTime),
+    updateTimeText: formatDateTime(item.updateTime),
+    currentStatusText: getCurrentStatusText(item.equipmentStatus?.currentStatus),
+    onlineStatusText: getOnlineStatusText(item.equipmentStatus?.onlineStatus),
+    alarmStatusText: getAlarmStatusText(item.equipmentStatus?.alarmStatus),
+    statusUpdateTimeText: formatDateTime(item.equipmentStatus?.statusUpdateTime)
+  }
+}
+
+function isOverdue(item) {
+  if (!item.planFinishTime) return false
+  return new Date(item.planFinishTime).getTime() < Date.now() && ![6, 7].includes(Number(item.orderStatus))
+}
+
+const filteredOrders = computed(() => {
+  let list = orders.value
+  if (statusFilter.value === 'pending') {
+    list = list.filter(item => item.orderStatusKey === 'pending')
+  } else if (statusFilter.value === 'processing') {
+    list = list.filter(item => item.orderStatusKey === 'processing' && !isOverdue(item))
+  } else if (statusFilter.value === 'overdue') {
+    list = list.filter(item => isOverdue(item))
+  }
   return list
 })
 
@@ -284,221 +341,185 @@ const paginatedOrders = computed(() => {
   return filteredOrders.value.slice(start, start + pageSize.value)
 })
 
-const totalOrders = computed(() => mockOrders.value.length)
-const pendingOrdersCount = computed(() => mockOrders.value.filter(o => o.status === 'pending').length)
-const processingOrdersCount = computed(() => mockOrders.value.filter(o => o.status === 'processing').length)
-const overdueCount = computed(() => mockOrders.value.filter(o => o.status === 'overdue').length)
+const totalOrders = computed(() => orders.value.length)
+const pendingOrdersCount = computed(() => orders.value.filter(item => item.orderStatusKey === 'pending').length)
+const processingOrdersCount = computed(() => orders.value.filter(item => item.orderStatusKey === 'processing' && !isOverdue(item)).length)
+const overdueCount = computed(() => orders.value.filter(item => isOverdue(item)).length)
 
-// 方法
-const setStatusFilter = (status) => {
-  statusFilter.value = status
-  currentPage.value = 1
-}
-
-const getStatusType = (status) => {
-  const map = { pending: 'danger', processing: 'warning', overdue: 'danger', completed: 'success' }
-  return map[status] || 'info'
-}
-
-const getStatusText = (status) => {
-  const map = { pending: '待处理', processing: '处理中', overdue: '已逾期', completed: '已完成' }
-  return map[status] || status
-}
-
-const refreshList = () => {
+async function loadOrders() {
   loading.value = true
-  setTimeout(() => {
+  try {
+    const keyword = searchKeyword.value.trim()
+    const payload = {
+      pageNum: 1,
+      pageSize: 200,
+      orderLevel: orderLevelFilter.value || undefined,
+      orderNo: keyword || undefined,
+      caseNo: keyword || undefined
+    }
+    const res = await getManholeWorkOrderProcessingPage(payload)
+    const records = Array.isArray(res?.data?.records) ? res.data.records : []
+    orders.value = records.map(normalizeOrder)
+    currentPage.value = 1
+    if (currentOrder.value) {
+      const matched = orders.value.find(item => item.orderId === currentOrder.value.orderId)
+      if (matched) {
+        await viewDetail(matched)
+      } else {
+        currentOrder.value = null
+        detailDrawerVisible.value = false
+      }
+    }
+  } catch (error) {
+    orders.value = []
+    currentOrder.value = null
+    ElMessage.error('经办工单加载失败')
+  } finally {
     loading.value = false
-    ElMessage.success('列表已刷新')
-  }, 500)
-}
-
-const handleRowClick = (row) => {
-  viewDetail(row)
+  }
 }
 
-const viewDetail = (order) => {
-  currentOrder.value = order
+async function viewDetail(order) {
+  detailLoading.value = true
   detailDrawerVisible.value = true
+  try {
+    const res = await getManholeWorkOrderDetail(order.orderId)
+    currentOrder.value = normalizeOrder(res?.data || order)
+  } catch (error) {
+    currentOrder.value = order
+    ElMessage.error('工单详情加载失败')
+  } finally {
+    detailLoading.value = false
+  }
 }
 
-const viewCaseDetail = () => {
-  ElMessage.info(`查看案件 ${currentOrder.value?.caseCode} 详情(演示)`)
+function handleRowClick(row) {
+  viewDetail(row)
 }
 
-const handleOrder = (order) => {
-  if (order.status === 'pending') {
-    // 如果未指派人员,先弹出指派对话框
-    if (!order.assignee) {
-      currentOrder.value = order
-      assignDialogVisible.value = true
+function setStatusFilter(status) {
+  statusFilter.value = status
+  currentPage.value = 1
+  if (detailDrawerVisible.value && currentOrder.value) {
+    const first = filteredOrders.value[0]
+    if (first) {
+      viewDetail(first)
     } else {
-      // 已有指派人,直接更新状态
-      order.status = 'processing'
-      order.processLogs.push({
-        id: Date.now(),
-        time: new Date().toLocaleString(),
-        content: `开始处理工单,负责人:${order.assignee}`,
-        type: 'primary'
-      })
-      ElMessage.success(`工单 ${order.code} 已开始处理`)
+      currentOrder.value = null
       detailDrawerVisible.value = false
     }
   }
 }
 
-const openAssignDialog = () => {
-  assignDialogVisible.value = true
-}
-
-const confirmAssign = () => {
-  if (!selectedWorkerId.value) {
-    ElMessage.warning('请选择维护人员')
-    return
-  }
-  const worker = workers.value.find(w => w.id === selectedWorkerId.value)
-  if (currentOrder.value) {
-    currentOrder.value.assignee = worker.name
-    currentOrder.value.status = 'processing'
-    currentOrder.value.processLogs.push({
-      id: Date.now(),
-      time: new Date().toLocaleString(),
-      content: `已指派 ${worker.name} 处理工单,备注:${assignRemark.value || '无'}`,
-      type: 'primary'
-    })
-    ElMessage.success(`工单已指派给 ${worker.name}`)
-  }
-  assignDialogVisible.value = false
-  selectedWorkerId.value = ''
-  assignRemark.value = ''
-  detailDrawerVisible.value = false
-}
-</script>
-
-<style scoped>
-.pending-orders {
-  background: #f0f2f6;
-  min-height: 100vh;
-  padding: 20px;
-}
-
-.page-header {
-  background: white;
-  border-radius: 16px;
-  padding: 16px 24px;
-  margin-bottom: 20px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 12px;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.04);
-}
-
-.header-left {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-
-.header-left .title {
-  font-size: 20px;
-  font-weight: 600;
-  color: #1f2d3d;
+function canStartRepair(order) {
+  return Number(order.orderStatus) === 3
 }
 
-.header-right {
-  display: flex;
-  gap: 12px;
-  align-items: center;
-  flex-wrap: wrap;
+function canAcceptOrder(order) {
+  return Number(order.orderStatus) === 2
 }
 
-.stats-cards {
-  display: flex;
-  gap: 16px;
-  margin-bottom: 20px;
-  flex-wrap: wrap;
+function canSubmitVerify(order) {
+  return Number(order.orderStatus) === 4
 }
 
-.stat-card {
-  background: white;
-  border-radius: 12px;
-  padding: 12px 24px;
-  text-align: center;
-  cursor: pointer;
-  transition: all 0.2s;
-  min-width: 100px;
-  box-shadow: 0 1px 4px rgba(0,0,0,0.05);
+function canDelay(order) {
+  return [3, 4].includes(Number(order.orderStatus))
 }
 
-.stat-card:hover {
-  transform: translateY(-2px);
-  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
+function hasOperationButtons(order) {
+  if (!order) return false
+  return canAcceptOrder(order) || canStartRepair(order) || canSubmitVerify(order) || canDelay(order)
 }
 
-.stat-card.active {
-  border: 2px solid #409eff;
-  background: #ecf5ff;
-}
-
-.stat-card.pending .stat-value { color: #f56c6c; }
-.stat-card.processing .stat-value { color: #e6a23c; }
-.stat-card.overdue .stat-value { color: #f56c6c; }
-
-.stat-value {
-  font-size: 28px;
-  font-weight: bold;
-}
-
-.stat-label {
-  font-size: 13px;
-  color: #606266;
-  margin-top: 4px;
-}
-
-.order-table-container {
-  background: white;
-  border-radius: 16px;
-  padding: 16px;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.04);
+async function operateOrder(orderId, operType, operDesc, extra = {}) {
+  await operManholeWorkOrder({
+    orderId,
+    operType,
+    operDesc,
+    operUserId: DEFAULT_OPER_USER_ID,
+    ...extra
+  })
+  await loadOrders()
+  if (currentOrder.value?.orderId === orderId) {
+    const matched = orders.value.find(item => item.orderId === orderId)
+    if (matched) {
+      await viewDetail(matched)
+    }
+  }
 }
 
-.pagination-wrapper {
-  display: flex;
-  justify-content: flex-end;
-  margin-top: 16px;
-  padding-top: 12px;
-  border-top: 1px solid #e9ecef;
+async function startRepair(order) {
+  try {
+    await operateOrder(order.orderId, 4, '开始维修')
+    ElMessage.success('已开始维修')
+  } catch (error) {
+    ElMessage.error(error?.message || '开始维修失败')
+  }
 }
 
-.order-detail {
-  padding: 8px;
+async function acceptOrder(order) {
+  try {
+    await operateOrder(order.orderId, 3, '接单')
+    ElMessage.success('已接单')
+  } catch (error) {
+    ElMessage.error(error?.message || '接单失败')
+  }
 }
 
-.sub-title {
-  font-weight: 600;
-  margin: 16px 0 12px 0;
-  font-size: 14px;
-  color: #2c3e50;
+async function submitVerify(order) {
+  try {
+    await operateOrder(order.orderId, 5, '提交验收')
+    ElMessage.success('已提交验收')
+  } catch (error) {
+    ElMessage.error(error?.message || '提交验收失败')
+  }
 }
 
-.process-logs {
-  margin-top: 16px;
+function openDelayDialog(order) {
+  currentOrder.value = order
+  delayDeadline.value = ''
+  delayReason.value = ''
+  delayDialogVisible.value = true
 }
 
-.detail-actions {
-  margin-top: 24px;
-  display: flex;
-  gap: 12px;
-  justify-content: flex-end;
+async function submitDelay() {
+  if (!currentOrder.value?.orderId) return
+  if (!delayDeadline.value) {
+    ElMessage.warning('请选择延期截止时间')
+    return
+  }
+  if (!delayReason.value.trim()) {
+    ElMessage.warning('请输入延期原因')
+    return
+  }
+  try {
+    await operateOrder(currentOrder.value.orderId, 8, '申请延期', {
+      delayedReason: delayReason.value.trim(),
+      delayedDeadline: delayDeadline.value
+    })
+    delayDialogVisible.value = false
+    ElMessage.success('延期申请已提交')
+  } catch (error) {
+    ElMessage.error(error?.message || '延期申请失败')
+  }
 }
 
-:deep(.el-table__row) {
-  cursor: pointer;
+function getLogType(operType) {
+  const map = {
+    2: 'primary',
+    3: 'warning',
+    4: 'primary',
+    5: 'warning',
+    6: 'success',
+    7: 'danger',
+    8: 'warning',
+    9: 'success'
+  }
+  return map[Number(operType)] || 'info'
 }
 
-:deep(.el-table__row:hover) {
-  background-color: #f5f7fa;
-}
-</style>
+onMounted(() => {
+  loadOrders()
+})
+</script>

+ 579 - 379
src/views/subSystem/manholeCover/caseHandling/orderDisposal.vue

@@ -1,180 +1,175 @@
 <!--工单处置管理-->
 <template>
-  <div class="workorder-management">
-    <!-- 页面头部统计 -->
-    <div class="page-header">
-      <div class="header-title">
+  <div class="min-h-screen bg-[#f0f2f6] p-5">
+    <div class="mb-5 flex flex-wrap items-center justify-between gap-4 rounded-[20px] bg-white px-6 py-4">
+      <div class="flex items-center gap-[14px] text-[20px] font-600">
         <el-icon><Tickets /></el-icon>
         <span>工单任务管理中心</span>
         <el-tag type="primary" effect="dark">待办工单 {{ pendingCount }}</el-tag>
       </div>
-      <div class="header-stats">
-        <div class="stat" @click="filterStatus('')">
-          <div class="num">{{ totalOrders }}</div>
-          <div>全部工单</div>
-        </div>
-        <div class="stat" @click="filterStatus('pending')">
-          <div class="num warning">{{ pendingCount }}</div>
-          <div>待处理</div>
-        </div>
-        <div class="stat" @click="filterStatus('processing')">
-          <div class="num">{{ processingCount }}</div>
-          <div>处理中</div>
-        </div>
-        <div class="stat" @click="filterStatus('feedback')">
-          <div class="num">{{ feedbackCount }}</div>
-          <div>待审批</div>
-        </div>
-        <div class="stat" @click="filterStatus('completed')">
-          <div class="num">{{ completedCount }}</div>
-          <div>已完成</div>
-        </div>
+      <div class="flex flex-wrap gap-3">
+        <el-input
+          v-model="searchKeyword"
+          placeholder="搜索工单编号/案件编号"
+          clearable
+          :prefix-icon="Search"
+          class="w-[220px]"
+          @keyup.enter="loadOrders"
+        />
+        <el-button type="primary" @click="loadOrders">刷新</el-button>
       </div>
     </div>
 
-    <div class="main-layout">
-      <!-- 左侧工单列表 -->
-      <div class="order-list-panel">
-        <div class="list-header">
+    <div class="mb-5 flex flex-wrap gap-6">
+      <div class="min-w-[120px] cursor-pointer rounded-2xl bg-white px-4 py-3 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === '' }" @click="filterStatus('')">
+        <div class="text-[26px] font-bold">{{ totalOrders }}</div>
+        <div>全部工单</div>
+      </div>
+      <div class="min-w-[120px] cursor-pointer rounded-2xl bg-white px-4 py-3 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === 'pending' }" @click="filterStatus('pending')">
+        <div class="text-[26px] font-bold text-[#e6a23c]">{{ pendingCount }}</div>
+        <div>待处理</div>
+      </div>
+      <div class="min-w-[120px] cursor-pointer rounded-2xl bg-white px-4 py-3 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === 'processing' }" @click="filterStatus('processing')">
+        <div class="text-[26px] font-bold">{{ processingCount }}</div>
+        <div>处理中</div>
+      </div>
+      <div class="min-w-[120px] cursor-pointer rounded-2xl bg-white px-4 py-3 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === 'verify' }" @click="filterStatus('verify')">
+        <div class="text-[26px] font-bold">{{ verifyCount }}</div>
+        <div>待验收</div>
+      </div>
+      <div class="min-w-[120px] cursor-pointer rounded-2xl bg-white px-4 py-3 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === 'completed' }" @click="filterStatus('completed')">
+        <div class="text-[26px] font-bold">{{ completedCount }}</div>
+        <div>已完成</div>
+      </div>
+    </div>
+
+    <div class="flex gap-5">
+      <div class="w-[380px] flex flex-col overflow-hidden rounded-[20px] bg-white" v-loading="loading">
+        <div class="flex justify-between border-b border-[#e9ecef] px-4 py-4 font-600">
           <span>工单列表</span>
-          <el-input v-model="searchKeyword" placeholder="搜索工单/案件编号" prefix-icon="Search" clearable size="small" style="width: 180px" />
         </div>
-        <div class="order-list">
+        <div class="flex-1 overflow-y-auto p-3">
           <div
-              v-for="order in paginatedOrders"
-              :key="order.id"
-              class="order-item"
-              :class="{ active: selectedOrder?.id === order.id, [order.status]: true }"
-              @click="selectOrder(order)"
+            v-for="order in paginatedOrders"
+            :key="order.orderId"
+            class="mb-2 cursor-pointer rounded-[14px] border border-transparent bg-[#fafbfc] p-3"
+            :class="{ 'border-[#409eff] bg-[#ecf5ff]': selectedOrder?.orderId === order.orderId }"
+            @click="selectOrder(order)"
           >
-            <div class="order-header">
-              <span class="order-code">{{ order.code }}</span>
-              <el-tag :type="getStatusType(order.status)" size="small">{{ getStatusText(order.status) }}</el-tag>
+            <div class="mb-1.5 flex justify-between">
+              <span class="font-600">{{ order.orderNo }}</span>
+              <el-tag :type="order.orderStatusTag" size="small">{{ order.orderStatusText }}</el-tag>
             </div>
-            <div class="order-case">关联案件: {{ order.caseCode }}</div>
-            <div class="order-location">{{ order.address }}</div>
-            <div class="order-time">创建: {{ order.createTime }}</div>
+            <div class="text-[12px] text-[#666]">关联案件: {{ order.caseNo || '—' }}</div>
+            <div class="my-1 text-[12px] text-[#666]">设备编号: {{ order.deviceCode || '—' }}</div>
+            <div class="text-[12px] text-[#666]">创建: {{ order.createTimeText }}</div>
           </div>
+          <el-empty v-if="!paginatedOrders.length" description="暂无工单数据" :image-size="80" />
         </div>
-        <div class="pagination-area">
+        <div class="border-t border-[#eee] p-3 text-center">
           <el-pagination small layout="prev, pager, next" :total="filteredOrders.length" :page-size="pageSize" v-model:current-page="currentPage" />
         </div>
       </div>
 
-      <!-- 右侧工单详情与操作区 -->
-      <div class="detail-panel" v-if="selectedOrder">
+      <div class="flex-1 overflow-y-auto rounded-[20px] bg-white p-5" v-if="selectedOrder" v-loading="detailLoading">
         <el-tabs v-model="activeTab">
-          <!-- 工单基本信息与案件信息 -->
           <el-tab-pane label="工单信息" name="info">
-            <div class="info-section">
-              <div class="section-title">
-                <span><el-icon><Document /></el-icon> 工单基本信息</span>
-                <div class="assign-area">
-                  <el-select v-model="selectedWorker" placeholder="选择维护人员" size="small" style="width: 160px" clearable>
-                    <el-option v-for="worker in workers" :key="worker.id" :label="worker.name" :value="worker.id" />
-                  </el-select>
-                  <el-button type="primary" size="small" @click="assignWorker">下派任务</el-button>
-                </div>
-              </div>
-              <el-descriptions :column="2" border>
-                <el-descriptions-item label="工单编号">{{ selectedOrder.code }}</el-descriptions-item>
-                <el-descriptions-item label="工单状态">{{ getStatusText(selectedOrder.status) }}</el-descriptions-item>
-                <el-descriptions-item label="关联案件编号">
-                  <el-link type="primary" @click="viewCase">{{ selectedOrder.caseCode }}</el-link>
-                </el-descriptions-item>
-                <el-descriptions-item label="报警级别">
-                  <el-tag :type="selectedOrder.alertLevel === 'critical' ? 'danger' : 'warning'" size="small">
-                    {{ selectedOrder.alertLevel === 'critical' ? '严重' : '一般' }}
-                  </el-tag>
-                </el-descriptions-item>
-                <el-descriptions-item label="报警类型">{{ selectedOrder.alertType }}</el-descriptions-item>
-                <el-descriptions-item label="权属单位">{{ selectedOrder.ownerUnit }}</el-descriptions-item>
-                <el-descriptions-item label="报警描述" :span="2">{{ selectedOrder.alertDesc }}</el-descriptions-item>
-                <el-descriptions-item label="详细地址" :span="2">{{ selectedOrder.address }}</el-descriptions-item>
-                <el-descriptions-item label="用途">{{ selectedOrder.purpose || '市政排水' }}</el-descriptions-item>
-                <el-descriptions-item label="指派人">{{ selectedOrder.assignee || '未指派' }}</el-descriptions-item>
-              </el-descriptions>
+            <div class="mb-3 mt-4 flex items-center gap-2 text-[16px] font-600">
+              <span><el-icon><Document /></el-icon> 工单基本信息</span>
             </div>
+            <el-descriptions :column="2" border>
+              <el-descriptions-item label="工单ID">{{ selectedOrder.orderId || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="工单编号">{{ selectedOrder.orderNo }}</el-descriptions-item>
+              <el-descriptions-item label="报警ID">{{ selectedOrder.alarmId || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="工单状态">
+                <el-tag :type="selectedOrder.orderStatusTag">{{ selectedOrder.orderStatusText }}</el-tag>
+              </el-descriptions-item>
+              <el-descriptions-item label="案件ID">{{ selectedOrder.caseId || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="关联案件编号">{{ selectedOrder.caseNo || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="设备ID">{{ selectedOrder.deviceId || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="设备编号">{{ selectedOrder.deviceCode || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="优先级">
+                <el-tag :type="selectedOrder.orderLevelTag">{{ selectedOrder.orderLevelText }}</el-tag>
+              </el-descriptions-item>
+              <el-descriptions-item label="工单类型">{{ selectedOrder.orderTypeText }}</el-descriptions-item>
+              <el-descriptions-item label="问题描述" :span="2">{{ selectedOrder.orderDesc || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="负责部门ID">{{ selectedOrder.deptId || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="设备类别">{{ selectedOrder.equipmentTypeName || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="接单人ID">{{ selectedOrder.receiveUser || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="派单人ID">{{ selectedOrder.dispatchUser || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="派单时间">{{ selectedOrder.dispatchTimeText }}</el-descriptions-item>
+              <el-descriptions-item label="创建时间">{{ selectedOrder.createTimeText }}</el-descriptions-item>
+              <el-descriptions-item label="计划完成时间">{{ selectedOrder.planFinishTimeText }}</el-descriptions-item>
+              <el-descriptions-item label="延期原因">{{ selectedOrder.delayedReason || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="延期截止时间">{{ selectedOrder.delayedDeadlineText }}</el-descriptions-item>
+              <el-descriptions-item label="验收人ID">{{ selectedOrder.verifyUser || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="验收时间">{{ selectedOrder.verifyTimeText }}</el-descriptions-item>
+              <el-descriptions-item label="实际完成时间">{{ selectedOrder.realFinishTimeText }}</el-descriptions-item>
+              <el-descriptions-item label="运维状态">{{ selectedOrder.currentStatusText }}</el-descriptions-item>
+              <el-descriptions-item label="在线状态">{{ selectedOrder.onlineStatusText }}</el-descriptions-item>
+              <el-descriptions-item label="报警状态">{{ selectedOrder.alarmStatusText }}</el-descriptions-item>
+              <el-descriptions-item label="状态更新时间">{{ selectedOrder.statusUpdateTimeText }}</el-descriptions-item>
+              <el-descriptions-item label="处置结果" :span="2">{{ selectedOrder.repairResult || '—' }}</el-descriptions-item>
+              <el-descriptions-item label="更新时间">{{ selectedOrder.updateTimeText }}</el-descriptions-item>
+            </el-descriptions>
 
-            <!-- 延期/缓办操作 -->
-            <div class="delay-section">
-              <div class="section-title"><el-icon><Timer /></el-icon> 工单延期/缓办</div>
-              <div class="delay-buttons">
-                <el-button size="small" type="warning" @click="openDelayDialog('延期')">申请延期</el-button>
-                <el-button size="small" type="info" @click="openDelayDialog('缓办')">申请缓办</el-button>
+            <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>
               </div>
-              <div v-if="selectedOrder.delayRecords?.length" class="delay-history">
-                <div v-for="(rec, idx) in selectedOrder.delayRecords" :key="idx" class="delay-item">
-                  {{ rec.type }} 至 {{ rec.deadline }},理由: {{ rec.reason }}
-                </div>
-              </div>
-            </div>
-          </el-tab-pane>
-
-          <!-- 处置反馈与审批 -->
-          <el-tab-pane label="处置反馈与审批" name="feedback">
-            <div class="feedback-section">
-              <div class="section-title"><el-icon><Picture /></el-icon> 现场处置反馈(负责人上报)</div>
-              <div v-if="selectedOrder.feedback">
-                <el-descriptions :column="1" border>
-                  <el-descriptions-item label="处置人员">{{ selectedOrder.feedback.workerName }}</el-descriptions-item>
-                  <el-descriptions-item label="处置时间">{{ selectedOrder.feedback.feedbackTime }}</el-descriptions-item>
-                  <el-descriptions-item label="处置说明">{{ selectedOrder.feedback.description }}</el-descriptions-item>
-                  <el-descriptions-item label="现场照片">
-                    <div class="feedback-images">
-                      <el-image v-for="(img, idx) in selectedOrder.feedback.images" :key="idx" :src="img" fit="cover" class="feedback-img" preview-teleported />
-                    </div>
-                  </el-descriptions-item>
-                </el-descriptions>
-                <!-- 周边视频监控 -->
-                <div class="video-section" v-if="selectedOrder.surroundVideo">
-                  <div class="sub-title"><el-icon><VideoCamera /></el-icon> 井盖周边视频监控</div>
-                  <div class="video-preview" @click="playVideo">点击查看周边监控录像</div>
-                </div>
+              <div class="mb-3 flex items-center gap-3" v-if="canDispatch(selectedOrder)">
+                <el-input v-model="receiveUserDisplay" readonly placeholder="请选择接单人" class="w-[260px]" />
+                <el-button @click="openDispatchDialog">选择人员</el-button>
+                <el-button type="primary" @click="dispatchOrder">下派任务</el-button>
               </div>
-              <div v-else class="no-feedback">暂无处置反馈信息,等待负责人上报</div>
-
-              <!-- 审批操作栏 -->
-              <div class="approval-section" v-if="selectedOrder.status === 'feedback'">
-                <div class="section-title"><el-icon><Checked /></el-icon> 工单审批</div>
-                <div class="approval-buttons">
-                  <el-button type="success" @click="approveOrder(true)">审批通过</el-button>
-                  <el-button type="danger" @click="approveOrder(false)">驳回重办</el-button>
-                </div>
-                <el-input type="textarea" v-model="approvalRemark" placeholder="审批意见(可选)" rows="2" style="margin-top: 12px" />
-              </div>
-              <div v-else-if="selectedOrder.status === 'completed'" class="approved-info">
-                <el-alert title="工单已完成审批" type="success" :closable="false" show-icon />
+              <div class="flex flex-wrap gap-3">
+                <el-button v-if="canStartRepair(selectedOrder)" type="primary" @click="startRepair">开始维修</el-button>
+                <el-button v-if="canSubmitVerify(selectedOrder)" type="warning" @click="submitVerify">提交验收</el-button>
+                <el-button v-if="canApprove(selectedOrder)" type="success" @click="approveOrder(true)">验收通过</el-button>
+                <el-button v-if="canApprove(selectedOrder)" type="danger" @click="approveOrder(false)">驳回工单</el-button>
+                <el-button v-if="canApproveDelay(selectedOrder)" type="success" @click="approveDelay">延期审核通过</el-button>
+                <el-button v-if="canRejectDelay(selectedOrder)" type="danger" @click="rejectDelay">驳回延期</el-button>
+                <el-button v-if="canDelay(selectedOrder)" type="info" plain @click="openDelayDialog">申请延期</el-button>
               </div>
+              <el-empty v-if="!hasOperationButtons(selectedOrder)" description="当前状态暂无可操作项" :image-size="80" />
             </div>
           </el-tab-pane>
 
-          <!-- 工单处理历史 -->
           <el-tab-pane label="处理历史" name="history">
-            <el-timeline>
-              <el-timeline-item v-for="log in selectedOrder.processLogs" :key="log.id" :timestamp="log.time" :type="log.type">
-                {{ log.content }}
-                <span v-if="log.operator" class="operator">操作人: {{ log.operator }}</span>
+            <el-timeline v-if="selectedOrder.workOrderLogList?.length">
+              <el-timeline-item
+                v-for="log in selectedOrder.workOrderLogList"
+                :key="log.logId"
+                :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>
               </el-timeline-item>
             </el-timeline>
+            <el-empty v-else description="暂无工单日志" :image-size="80" />
           </el-tab-pane>
         </el-tabs>
       </div>
 
-      <!-- 未选中状态 -->
-      <div v-else class="empty-panel">
+      <div v-else class="flex flex-1 items-center justify-center rounded-[20px] bg-white">
         <el-empty description="请从左侧选择工单进行处理" :image-size="140" />
       </div>
     </div>
 
-    <!-- 延期/缓办对话框 -->
-    <el-dialog v-model="delayDialogVisible" :title="delayType + '申请'" width="450px">
-      <el-form>
-        <el-form-item :label="delayType === '延期' ? '延期截止时间' : '缓办截止时间'">
-          <el-date-picker v-model="delayDeadline" type="datetime" placeholder="选择截止时间" format="YYYY-MM-DD HH:mm" />
+    <el-dialog v-model="delayDialogVisible" title="申请延期" width="450px">
+      <el-form label-width="88px">
+        <el-form-item label="截止时间">
+          <el-date-picker
+            v-model="delayDeadline"
+            type="datetime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            placeholder="选择截止时间"
+            class="w-full"
+          />
         </el-form-item>
         <el-form-item label="申请理由">
-          <el-input type="textarea" v-model="delayReason" placeholder="请详细说明延期或缓办理由" rows="3" />
+          <el-input type="textarea" v-model="delayReason" placeholder="请详细说明延期理由" rows="3" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -182,286 +177,491 @@
         <el-button type="primary" @click="submitDelay">提交申请</el-button>
       </template>
     </el-dialog>
+
+    <el-dialog v-model="dispatchDialogVisible" title="下派任务" width="860px">
+      <div class="flex gap-4">
+        <div class="w-[260px] overflow-auto rounded-lg border border-[#ebeef5] p-3">
+          <div class="mb-3 font-600">部门树</div>
+          <el-input
+            v-model="deptKeyword"
+            placeholder="筛选部门"
+            clearable
+            size="small"
+            class="mb-3"
+          />
+          <el-tree
+            ref="deptTreeRef"
+            :data="deptOptions"
+            node-key="id"
+            :props="{ label: 'label', children: 'children' }"
+            default-expand-all
+            highlight-current
+            :filter-node-method="filterDeptNode"
+            @node-click="handleDeptClick"
+          />
+        </div>
+        <div class="flex-1 rounded-lg border border-[#ebeef5] p-3">
+          <div class="mb-3 font-600">用户列表</div>
+          <el-table
+            :data="dispatchUsers"
+            height="360"
+            border
+            highlight-current-row
+            v-loading="dispatchUserLoading"
+            @current-change="handleUserCurrentChange"
+            @row-click="handleUserCurrentChange"
+          >
+            <el-table-column prop="userId" label="ID" width="90" />
+            <el-table-column prop="userName" label="用户名" min-width="120" />
+            <el-table-column prop="nickName" label="昵称" min-width="120" />
+            <el-table-column prop="dept.deptName" label="部门" min-width="140" show-overflow-tooltip />
+          </el-table>
+          <div class="mt-3 text-[#606266]">
+            已选人员:{{ dispatchSelectedUser ? `${dispatchSelectedUser.userName} / ID: ${dispatchSelectedUser.userId}` : '未选择' }}
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <el-button @click="dispatchDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmDispatchUser">确认选择</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
-import { Tickets, Search, Document, Timer, Picture, VideoCamera, Checked } from '@element-plus/icons-vue'
-import { ElMessage } from 'element-plus'
-
-// 模拟维护人员列表
-const workers = ref([
-  { id: 'w1', name: '张建国 - 维修一组' },
-  { id: 'w2', name: '李志强 - 维修二组' },
-  { id: 'w3', name: '王大明 - 应急抢修队' },
-  { id: 'w4', name: '陈辉 - 巡检专员' }
-])
-
-// 模拟工单数据
-const mockOrders = ref([
-  {
-    id: 1, code: 'WO-20250402-001', caseCode: 'CASE-20250402-001', alertLevel: 'critical', alertType: '倾斜超标',
-    alertDesc: '井盖倾斜角度14.2°,超过阈值8°,有跌落风险', ownerUnit: '市政工程管理处', address: '人民路与解放路口东50m',
-    purpose: '主干道排水', status: 'pending', createTime: '2025-04-02 14:25:00', assignee: null,
-    processLogs: [{ id: 1, time: '2025-04-02 14:25:00', content: '工单自动创建', type: 'info', operator: '系统' }],
-    delayRecords: [], feedback: null
-  },
-  {
-    id: 2, code: 'WO-20250402-002', caseCode: 'CASE-20250402-002', alertLevel: 'critical', alertType: '水位超限',
-    alertDesc: '井下水位62cm,超过阈值40cm,溢水风险', ownerUnit: '水务集团', address: '滨江路化工园区南门',
-    purpose: '化工区雨水排放', status: 'processing', createTime: '2025-04-02 14:00:00', assignee: '张建国',
-    processLogs: [
-      { id: 1, time: '2025-04-02 14:00:00', content: '工单自动创建', type: 'info', operator: '系统' },
-      { id: 2, time: '2025-04-02 14:30:00', content: '已指派张建国前往处置', type: 'primary', operator: '调度中心' }
-    ],
-    delayRecords: [],
-    feedback: null
-  },
-  {
-    id: 3, code: 'WO-20250402-003', caseCode: 'CASE-20250402-003', alertLevel: 'warning', alertType: '异常震动',
-    alertDesc: '异常震动112mg,超过阈值80mg,疑似外力冲击', ownerUnit: '市政设施管理处', address: '古城街北段树下',
-    purpose: '老城区排水', status: 'feedback', createTime: '2025-04-02 12:15:00', assignee: '李志强',
-    surroundVideo: true,
-    feedback: {
-      workerName: '李志强', feedbackTime: '2025-04-02 15:20:00', description: '现场井盖因车辆碾轧移位,已复位加固,设备恢复正常',
-      images: ['https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg']
-    },
-    processLogs: [
-      { id: 1, time: '2025-04-02 12:15:00', content: '工单自动创建', type: 'info', operator: '系统' },
-      { id: 2, time: '2025-04-02 13:00:00', content: '指派李志强现场处置', type: 'primary', operator: '调度中心' },
-      { id: 3, time: '2025-04-02 15:20:00', content: '负责人提交处置反馈,待审批', type: 'warning', operator: '李志强' }
-    ],
-    delayRecords: []
-  },
-  {
-    id: 4, code: 'WO-20250402-004', caseCode: 'CASE-20250402-004', alertLevel: 'warning', alertType: '井盖开启异常',
-    alertDesc: '井盖非正常开启', ownerUnit: '市政工程管理处', address: '南港路旧车站段',
-    purpose: '市政井盖', status: 'completed', createTime: '2025-04-02 11:50:00', assignee: '王大明',
-    feedback: {
-      workerName: '王大明', feedbackTime: '2025-04-02 14:10:00', description: '经核实为附近施工人员误开,已关闭并加锁',
-      images: ['https://fuss10.elemecdn.com/3/5e/4bbf4c1b1b4f1e1c0b4b4c1b1b4f1e1c.jpeg']
-    },
-    processLogs: [
-      { id: 1, time: '2025-04-02 11:50:00', content: '工单创建', type: 'info', operator: '系统' },
-      { id: 2, time: '2025-04-02 13:00:00', content: '指派王大明处置', type: 'primary', operator: '调度中心' },
-      { id: 3, time: '2025-04-02 14:10:00', content: '提交反馈并审批通过', type: 'success', operator: '审批人' }
-    ],
-    delayRecords: [],
-    approved: true
-  }
-])
+import {computed, nextTick, onMounted, ref, watch} from 'vue'
+import {Document, Search, Tickets, Timer} from '@element-plus/icons-vue'
+import {ElMessage} from 'element-plus'
+import {getManholeWorkOrderDetail, getManholeWorkOrderPage, operManholeWorkOrder} from '@/api/pipeNetwork/basic'
+import {deptTreeSelect, listUser} from '@/api/system/user'
+
+const DEFAULT_OPER_USER_ID = 1
 
+const orders = ref([])
+const loading = ref(false)
+const detailLoading = ref(false)
 const searchKeyword = ref('')
 const statusFilter = ref('')
 const currentPage = ref(1)
 const pageSize = 5
-const selectedOrder = ref(mockOrders.value[0])
-const selectedWorker = ref('')
+const selectedOrder = ref(null)
 const activeTab = ref('info')
+const receiveUserId = ref(null)
+const receiveUserDisplay = ref('')
 const delayDialogVisible = ref(false)
-const delayType = ref('延期')
 const delayDeadline = ref('')
 const delayReason = ref('')
-const approvalRemark = ref('')
+const dispatchDialogVisible = ref(false)
+const dispatchUserLoading = ref(false)
+const deptOptions = ref([])
+const deptKeyword = ref('')
+const selectedDeptId = ref(undefined)
+const dispatchUsers = ref([])
+const dispatchSelectedUser = ref(null)
+const deptTreeRef = ref(null)
 
-// 过滤与分页
-const filteredOrders = computed(() => {
-  let list = mockOrders.value
-  if (statusFilter.value) list = list.filter(o => o.status === statusFilter.value)
-  if (searchKeyword.value) {
-    const kw = searchKeyword.value.toLowerCase()
-    list = list.filter(o => o.code.toLowerCase().includes(kw) || o.caseCode.toLowerCase().includes(kw))
+function formatDateTime(value) {
+  if (!value) return '—'
+  return String(value).replace('T', ' ')
+}
+
+function getOrderLevelMeta(level) {
+  const map = {
+    1: { text: '紧急', tag: 'danger' },
+    2: { text: '一般', tag: 'warning' },
+    3: { text: '低', tag: 'info' }
   }
-  return list
+  return map[Number(level)] || { text: '—', tag: 'info' }
+}
+
+function getOrderTypeText(type) {
+  const map = {
+    1: '故障维修',
+    2: '日常巡检',
+    3: '设备保养'
+  }
+  return map[Number(type)] || '—'
+}
+
+function getOrderStatusMeta(status) {
+  const map = {
+    1: { key: 'pending', text: '待派单', tag: 'danger' },
+    2: { key: 'pending', text: '待接单', tag: 'danger' },
+    3: { key: 'processing', text: '已接单', tag: 'warning' },
+    4: { key: 'processing', text: '处理中', tag: 'warning' },
+    5: { key: 'verify', text: '待验收', tag: 'primary' },
+    6: { key: 'completed', text: '已结案', tag: 'success' },
+    7: { key: 'completed', text: '已驳回', tag: 'danger' },
+    8: { key: 'processing', text: '延期审核', tag: 'warning' },
+    9: { key: 'completed', text: '已延期', tag: 'warning' }
+  }
+  return map[Number(status)] || { key: '', text: '—', tag: 'info' }
+}
+
+function getCurrentStatusText(status) {
+  const map = {
+    1: '在用',
+    2: '闲置',
+    3: '维修',
+    4: '报废',
+    5: '待入库'
+  }
+  return map[Number(status)] || '—'
+}
+
+function getOnlineStatusText(status) {
+  const map = {
+    0: '离线',
+    1: '在线'
+  }
+  return map[Number(status)] || '—'
+}
+
+function getAlarmStatusText(status) {
+  const map = {
+    0: '正常',
+    1: '报警'
+  }
+  return map[Number(status)] || '—'
+}
+
+function normalizeOrder(item) {
+  const levelMeta = getOrderLevelMeta(item.orderLevel)
+  const statusMeta = getOrderStatusMeta(item.orderStatus)
+  return {
+    ...item,
+    orderLevelText: levelMeta.text,
+    orderLevelTag: levelMeta.tag,
+    orderTypeText: getOrderTypeText(item.orderType),
+    orderStatusKey: statusMeta.key,
+    orderStatusText: statusMeta.text,
+    orderStatusTag: statusMeta.tag,
+    createTimeText: formatDateTime(item.createTime),
+    dispatchTimeText: formatDateTime(item.dispatchTime),
+    planFinishTimeText: formatDateTime(item.planFinishTime),
+    delayedDeadlineText: formatDateTime(item.delayedDeadline),
+    realFinishTimeText: formatDateTime(item.realFinishTime),
+    verifyTimeText: formatDateTime(item.verifyTime),
+    updateTimeText: formatDateTime(item.updateTime),
+    currentStatusText: getCurrentStatusText(item.equipmentStatus?.currentStatus),
+    onlineStatusText: getOnlineStatusText(item.equipmentStatus?.onlineStatus),
+    alarmStatusText: getAlarmStatusText(item.equipmentStatus?.alarmStatus),
+    statusUpdateTimeText: formatDateTime(item.equipmentStatus?.statusUpdateTime)
+  }
+}
+
+const filteredOrders = computed(() => {
+  if (!statusFilter.value) return orders.value
+  return orders.value.filter(item => item.orderStatusKey === statusFilter.value)
 })
+
 const paginatedOrders = computed(() => {
   const start = (currentPage.value - 1) * pageSize
   return filteredOrders.value.slice(start, start + pageSize)
 })
 
-const totalOrders = computed(() => mockOrders.value.length)
-const pendingCount = computed(() => mockOrders.value.filter(o => o.status === 'pending').length)
-const processingCount = computed(() => mockOrders.value.filter(o => o.status === 'processing').length)
-const feedbackCount = computed(() => mockOrders.value.filter(o => o.status === 'feedback').length)
-const completedCount = computed(() => mockOrders.value.filter(o => o.status === 'completed').length)
-
-const filterStatus = (status) => { statusFilter.value = status; currentPage.value = 1 }
-const selectOrder = (order) => { selectedOrder.value = order; selectedWorker.value = order.assignee ? workers.value.find(w => w.name === order.assignee)?.id || '' : '' }
-
-const getStatusType = (status) => {
-  if (status === 'pending') return 'danger'
-  if (status === 'processing') return 'warning'
-  if (status === 'feedback') return 'info'
-  return 'success'
-}
-const getStatusText = (status) => {
-  const map = { pending: '待处理', processing: '处理中', feedback: '待审批', completed: '已完成' }
-  return map[status] || status
-}
-
-// 下派任务
-const assignWorker = () => {
-  if (!selectedWorker.value) { ElMessage.warning('请选择维护人员'); return }
-  const worker = workers.value.find(w => w.id === selectedWorker.value)
-  if (selectedOrder.value.status === 'pending') {
-    selectedOrder.value.status = 'processing'
-    selectedOrder.value.assignee = worker.name
-    selectedOrder.value.processLogs.push({
-      id: Date.now(),
-      time: new Date().toLocaleString(),
-      content: `已下派任务给 ${worker.name}`,
-      type: 'primary',
-      operator: '平台调度员'
+const totalOrders = computed(() => orders.value.length)
+const pendingCount = computed(() => orders.value.filter(item => item.orderStatusKey === 'pending').length)
+const processingCount = computed(() => orders.value.filter(item => item.orderStatusKey === 'processing').length)
+const verifyCount = computed(() => orders.value.filter(item => item.orderStatusKey === 'verify').length)
+const completedCount = computed(() => orders.value.filter(item => item.orderStatusKey === 'completed').length)
+
+async function loadOrders() {
+  loading.value = true
+  try {
+    const keyword = searchKeyword.value.trim()
+    const res = await getManholeWorkOrderPage({
+      pageNum: 1,
+      pageSize: 200,
+      orderNo: keyword || undefined,
+      caseNo: keyword || undefined
     })
-    ElMessage.success(`工单已下派至 ${worker.name}`)
-  } else {
-    ElMessage.warning('当前工单状态不可下派')
+    const records = Array.isArray(res?.data?.records) ? res.data.records : []
+    orders.value = records.map(normalizeOrder)
+    currentPage.value = 1
+    const first = filteredOrders.value[0]
+    if (!first) {
+      selectedOrder.value = null
+      return
+    }
+    if (selectedOrder.value) {
+      const matched = orders.value.find(item => item.orderId === selectedOrder.value.orderId)
+      if (matched) {
+        await selectOrder(matched)
+        return
+      }
+    }
+    await selectOrder(first)
+  } catch (error) {
+    orders.value = []
+    selectedOrder.value = null
+    ElMessage.error('工单列表加载失败')
+  } finally {
+    loading.value = false
   }
 }
 
-// 延期/缓办
-const openDelayDialog = (type) => {
-  delayType.value = type
-  delayDialogVisible.value = true
+async function selectOrder(order) {
+  detailLoading.value = true
+  receiveUserId.value = order.receiveUser || null
+  receiveUserDisplay.value = order.receiveUser ? `用户ID: ${order.receiveUser}` : ''
+  try {
+    const res = await getManholeWorkOrderDetail(order.orderId)
+    selectedOrder.value = normalizeOrder(res?.data || order)
+    receiveUserId.value = selectedOrder.value.receiveUser || null
+    receiveUserDisplay.value = selectedOrder.value.receiveUser ? `用户ID: ${selectedOrder.value.receiveUser}` : ''
+  } catch (error) {
+    selectedOrder.value = order
+    ElMessage.error('工单详情加载失败')
+  } finally {
+    detailLoading.value = false
+  }
+}
+
+function filterStatus(status) {
+  statusFilter.value = status
+  currentPage.value = 1
+  const first = filteredOrders.value[0]
+  if (first) {
+    selectOrder(first)
+  }
+}
+
+function canDispatch(order) {
+  return Number(order.orderStatus) === 1
+}
+
+function canStartRepair(order) {
+  return [2, 3].includes(Number(order.orderStatus))
+}
+
+function canSubmitVerify(order) {
+  return Number(order.orderStatus) === 4
+}
+
+function canApprove(order) {
+  return Number(order.orderStatus) === 5
+}
+
+function canApproveDelay(order) {
+  return Number(order.orderStatus) === 8
+}
+
+function canRejectDelay(order) {
+  return Number(order.orderStatus) === 8
+}
+
+function canDelay(order) {
+  return [2, 3, 4].includes(Number(order.orderStatus))
 }
-const submitDelay = () => {
-  if (!delayDeadline.value) { ElMessage.warning('请选择截止时间'); return }
-  if (!delayReason.value) { ElMessage.warning('请填写申请理由'); return }
-  const record = {
-    type: delayType.value,
-    deadline: new Date(delayDeadline.value).toLocaleString(),
-    reason: delayReason.value,
-    applyTime: new Date().toLocaleString()
+
+function hasOperationButtons(order) {
+  if (!order) return false
+  return (
+    canDispatch(order) ||
+    canStartRepair(order) ||
+    canSubmitVerify(order) ||
+    canApprove(order) ||
+    canApproveDelay(order) ||
+    canRejectDelay(order) ||
+    canDelay(order)
+  )
+}
+
+async function operateCurrent(operType, operDesc, extra = {}) {
+  if (!selectedOrder.value?.orderId) return
+  await operManholeWorkOrder({
+    orderId: selectedOrder.value.orderId,
+    operType,
+    operDesc,
+    operUserId: DEFAULT_OPER_USER_ID,
+    ...extra
+  })
+  await loadOrders()
+}
+
+async function dispatchOrder() {
+  if (!receiveUserId.value) {
+    ElMessage.warning('请先选择接单人')
+    return
   }
-  if (!selectedOrder.value.delayRecords) selectedOrder.value.delayRecords = []
-  selectedOrder.value.delayRecords.push(record)
-  selectedOrder.value.processLogs.push({
-    id: Date.now(),
-    time: new Date().toLocaleString(),
-    content: `申请${delayType.value},截止时间:${record.deadline},理由:${delayReason.value}`,
-    type: 'warning',
-    operator: '工单负责人'
+  try {
+    await operateCurrent(2, `下派工单至用户${receiveUserId.value}`, { receiveUser: Number(receiveUserId.value) })
+    ElMessage.success('工单下派成功')
+  } catch (error) {
+    ElMessage.error(error?.message || '工单下派失败')
+  }
+}
+
+function filterDeptNode(value, data) {
+  if (!value) return true
+  return data.label?.includes(value)
+}
+
+watch(deptKeyword, value => {
+  nextTick(() => {
+    deptTreeRef.value?.filter(value)
   })
-  ElMessage.success(`${delayType.value}申请已提交,待审批`)
-  delayDialogVisible.value = false
+})
+
+async function loadDeptTree() {
+  const res = await deptTreeSelect()
+  deptOptions.value = Array.isArray(res?.data) ? res.data : []
+}
+
+async function loadDispatchUsers(deptId) {
+  dispatchUserLoading.value = true
+  try {
+    const res = await listUser({
+      pageNum: 1,
+      pageSize: 100,
+      deptId,
+      status: '0'
+    })
+    dispatchUsers.value = Array.isArray(res?.rows) ? res.rows : []
+  } catch (error) {
+    dispatchUsers.value = []
+    ElMessage.error('用户列表加载失败')
+  } finally {
+    dispatchUserLoading.value = false
+  }
+}
+
+async function openDispatchDialog() {
+  dispatchDialogVisible.value = true
+  dispatchSelectedUser.value = null
+  dispatchUsers.value = []
+  deptKeyword.value = ''
+  if (!deptOptions.value.length) {
+    await loadDeptTree()
+  }
+  const firstDept = findFirstDept(deptOptions.value)
+  if (firstDept?.id) {
+    selectedDeptId.value = firstDept.id
+    await nextTick()
+    deptTreeRef.value?.setCurrentKey(firstDept.id)
+    await loadDispatchUsers(firstDept.id)
+  }
+}
+
+function findFirstDept(nodes = []) {
+  for (const node of nodes) {
+    if (node?.id) return node
+    if (node?.children?.length) {
+      const child = findFirstDept(node.children)
+      if (child) return child
+    }
+  }
+  return null
+}
+
+function handleDeptClick(data) {
+  selectedDeptId.value = data.id
+  loadDispatchUsers(data.id)
+}
+
+function handleUserCurrentChange(row) {
+  dispatchSelectedUser.value = row || null
+}
+
+function confirmDispatchUser() {
+  if (!dispatchSelectedUser.value?.userId) {
+    ElMessage.warning('请选择用户')
+    return
+  }
+  receiveUserId.value = dispatchSelectedUser.value.userId
+  receiveUserDisplay.value = `${dispatchSelectedUser.value.userName} / ID: ${dispatchSelectedUser.value.userId}`
+  dispatchDialogVisible.value = false
+}
+
+async function startRepair() {
+  try {
+    await operateCurrent(4, '开始维修')
+    ElMessage.success('已开始维修')
+  } catch (error) {
+    ElMessage.error(error?.message || '开始维修失败')
+  }
+}
+
+async function submitVerify() {
+  try {
+    await operateCurrent(5, '提交验收')
+    ElMessage.success('已提交验收')
+  } catch (error) {
+    ElMessage.error(error?.message || '提交验收失败')
+  }
+}
+
+async function approveOrder(isApproved) {
+  try {
+    await operateCurrent(isApproved ? 6 : 7, isApproved ? '验收通过' : '驳回工单')
+    ElMessage.success(isApproved ? '验收已通过' : '工单已驳回')
+  } catch (error) {
+    ElMessage.error(error?.message || '工单操作失败')
+  }
+}
+
+async function approveDelay() {
+  try {
+    await operateCurrent(9, '延期审核通过')
+    ElMessage.success('延期审核已通过')
+  } catch (error) {
+    ElMessage.error(error?.message || '延期审核通过失败')
+  }
+}
+
+async function rejectDelay() {
+  try {
+    await operateCurrent(7, '驳回延期')
+    ElMessage.success('延期申请已驳回')
+  } catch (error) {
+    ElMessage.error(error?.message || '驳回延期失败')
+  }
+}
+
+function openDelayDialog() {
   delayDeadline.value = ''
   delayReason.value = ''
+  delayDialogVisible.value = true
 }
 
-// 审批功能
-const approveOrder = (isApproved) => {
-  if (selectedOrder.value.status !== 'feedback') return
-  if (isApproved) {
-    selectedOrder.value.status = 'completed'
-    selectedOrder.value.processLogs.push({
-      id: Date.now(),
-      time: new Date().toLocaleString(),
-      content: `审批通过,工单办结。审批意见:${approvalRemark.value || '无'}`,
-      type: 'success',
-      operator: '审批人'
-    })
-    ElMessage.success('工单审批通过,已办结')
-  } else {
-    selectedOrder.value.status = 'processing'
-    selectedOrder.value.processLogs.push({
-      id: Date.now(),
-      time: new Date().toLocaleString(),
-      content: `审批驳回,需重新处置。驳回意见:${approvalRemark.value || '请重新反馈'}`,
-      type: 'danger',
-      operator: '审批人'
+async function submitDelay() {
+  if (!delayDeadline.value) {
+    ElMessage.warning('请选择截止时间')
+    return
+  }
+  if (!delayReason.value.trim()) {
+    ElMessage.warning('请填写延期理由')
+    return
+  }
+  try {
+    await operateCurrent(8, '申请延期', {
+      delayedDeadline: delayDeadline.value,
+      delayedReason: delayReason.value.trim()
     })
-    // 清除原反馈,等待重新上报
-    selectedOrder.value.feedback = null
-    ElMessage.warning('工单已驳回,请重新处置反馈')
+    delayDialogVisible.value = false
+    ElMessage.success('延期申请已提交')
+  } catch (error) {
+    ElMessage.error(error?.message || '延期申请失败')
   }
-  approvalRemark.value = ''
 }
 
-const viewCase = () => { ElMessage.info('跳转案件详情(演示)') }
-const playVideo = () => { ElMessage.info('模拟播放周边视频监控') }
+function getLogType(operType) {
+  const map = {
+    2: 'primary',
+    3: 'warning',
+    4: 'primary',
+    5: 'warning',
+    6: 'success',
+    7: 'danger',
+    8: 'warning',
+    9: 'success'
+  }
+  return map[Number(operType)] || 'info'
+}
 
+onMounted(() => {
+  loadOrders()
+})
 </script>
-
-<style scoped>
-.workorder-management {
-  background: #f0f2f6;
-  min-height: 100vh;
-  padding: 20px;
-}
-.page-header {
-  background: white;
-  border-radius: 20px;
-  padding: 16px 24px;
-  margin-bottom: 20px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-.header-title { display: flex; align-items: center; gap: 14px; font-size: 20px; font-weight: 600; }
-.header-stats { display: flex; gap: 24px; }
-.stat { text-align: center; cursor: pointer; padding: 4px 16px; border-radius: 30px; }
-.stat:hover { background: #ecf5ff; }
-.num { font-size: 26px; font-weight: bold; }
-.num.warning { color: #e6a23c; }
-.main-layout { display: flex; gap: 20px; }
-.order-list-panel {
-  width: 380px;
-  background: white;
-  border-radius: 20px;
-  display: flex;
-  flex-direction: column;
-  overflow: hidden;
-}
-.list-header {
-  padding: 16px;
-  border-bottom: 1px solid #e9ecef;
-  display: flex;
-  justify-content: space-between;
-  font-weight: 600;
-}
-.order-list { flex: 1; overflow-y: auto; padding: 12px; }
-.order-item {
-  padding: 12px;
-  margin-bottom: 8px;
-  border-radius: 14px;
-  background: #fafbfc;
-  cursor: pointer;
-  border: 1px solid transparent;
-}
-.order-item.active { background: #ecf5ff; border-color: #409eff; }
-.order-item.pending { border-left: 3px solid #f56c6c; }
-.order-item.processing { border-left: 3px solid #e6a23c; }
-.order-item.feedback { border-left: 3px solid #909399; }
-.order-header { display: flex; justify-content: space-between; margin-bottom: 6px; }
-.order-code { font-weight: 600; }
-.order-location { font-size: 12px; color: #666; margin: 4px 0; }
-.order-time { font-size: 11px; color: #aaa; }
-.detail-panel {
-  flex: 1;
-  background: white;
-  border-radius: 20px;
-  padding: 20px;
-  overflow-y: auto;
-}
-.section-title {
-  font-weight: 600;
-  font-size: 16px;
-  margin: 16px 0 12px 0;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-.assign-area { display: flex; gap: 8px; }
-.delay-buttons { display: flex; gap: 12px; margin-bottom: 12px; }
-.delay-history { background: #fef7e0; padding: 8px 12px; border-radius: 8px; font-size: 13px; }
-.feedback-images { display: flex; gap: 12px; margin-top: 8px; }
-.feedback-img { width: 100px; height: 80px; border-radius: 8px; object-fit: cover; }
-.video-preview { background: #eef2fa; padding: 10px; border-radius: 12px; text-align: center; cursor: pointer; margin-top: 12px; }
-.approval-section { margin-top: 20px; border-top: 1px solid #e9ecef; padding-top: 16px; }
-.approval-buttons { display: flex; gap: 12px; margin: 12px 0; }
-.approved-info { margin-top: 16px; }
-.empty-panel { flex: 1; display: flex; align-items: center; justify-content: center; background: white; border-radius: 20px; }
-.operator { font-size: 12px; color: #909399; margin-left: 12px; }
-.pagination-area { padding: 12px; text-align: center; border-top: 1px solid #eee; }
-</style>

+ 18 - 0
uno.config.js

@@ -0,0 +1,18 @@
+import {defineConfig, presetAttributify, presetUno, transformerDirectives} from 'unocss'
+
+export default defineConfig({
+  presets: [
+    presetUno(),
+    presetAttributify()
+  ],
+  transformers: [
+    transformerDirectives()
+  ],
+  content: {
+    pipeline: {
+      include: [
+        'src/**/*.{vue,js,ts,jsx,tsx}'
+      ]
+    }
+  }
+})

+ 2 - 1
vite/plugins/index.js

@@ -1,4 +1,5 @@
 import vue from '@vitejs/plugin-vue'
+import UnoCSS from '@unocss/vite'
 
 import createAutoImport from './auto-import'
 import createSvgIcon from './svg-icon'
@@ -6,7 +7,7 @@ import createCompression from './compression'
 import createSetupExtend from './setup-extend'
 
 export default function createVitePlugins(viteEnv, isBuild = false) {
-    const vitePlugins = [vue()]
+    const vitePlugins = [vue(), UnoCSS()]
     vitePlugins.push(createAutoImport())
 	vitePlugins.push(createSetupExtend())
     vitePlugins.push(createSvgIcon(isBuild))