Sfoglia il codice sorgente

管网子系统-报警相关功能开发

zengy 2 giorni fa
parent
commit
ef2b3a571e

+ 0 - 0
src/views/subSystem/drainage/alarm/alarmGIS.scss → src/views/subSystem/alarm/common/alarmGIS.scss


+ 3 - 2
src/views/subSystem/drainage/alarm/alarmGIS.vue → src/views/subSystem/alarm/common/alarmGIS.vue

@@ -2,7 +2,7 @@
   <div class="alarm-page alarm-gis-page">
     <header class="gis-page-heading">
       <div><h1>当前报警 GIS 一张图</h1><p>按设备查看报警分布,关联监测曲线与报警记录</p></div>
-      <span class="gis-view-label"><el-icon><MapLocation /></el-icon>排水监测视图</span>
+      <span class="gis-view-label"><el-icon><MapLocation /></el-icon>{{ route.meta.title || '报警GIS一张图' }}</span>
     </header>
 
     <section class="gis-filter-card">
@@ -105,6 +105,7 @@
 <script setup name="DrainageAlarmGIS">
 import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
 import { Search, Refresh, MapLocation, Location, Aim, Plus, Minus, ArrowRight, Warning, Loading } from '@element-plus/icons-vue'
+import { useRoute } from 'vue-router'
 import GisMapCanvas from '@/components/GisMapCanvas.vue'
 import { getAlarmMap, getAlarmOptions, getAlarms } from '@/api/alarm'
 import AlarmDetail from '../../alarm/AlarmDetail.vue'
@@ -116,6 +117,7 @@ import { useAlarmScope } from '../../alarm/common/js/useAlarmScope'
 import '../../alarm/common/styles/alarmOperations.scss'
 
 const { scope, scopeKey } = useAlarmScope()
+const route = useRoute()
 const baseUrl = import.meta.env.VITE_APP_BASE_API
 const keyword = ref(''), level = ref(), options = ref({}), rows = ref([]), selected = ref(null), tab = ref('curve'), curveId = ref('')
 const loading = ref(false), error = ref(''), mapError = ref(''), mapReady = ref(false), mapKey = ref(0), mapRef = ref(), detailRef = ref(), cluster = ref([])
@@ -167,7 +169,6 @@ function retryMap() { mapError.value = ''; mapReady.value = false; ++mapKey.valu
 function reset() { keyword.value = ''; level.value = undefined; return load() }
 watch(scopeKey, async () => {
   ++generation; rows.value = []; selected.value = null; detailVisible.value = false; loading.value = false; error.value = ''; options.value = {}
-  if (scope.value.rootTypeId !== 2) { error.value = '该地图仅适用于排水子系统'; return }
   const key = scopeKey.value
   try { const result = await getAlarmOptions({ ...scope.value }); if (key === scopeKey.value) options.value = result.data || {} }
   catch (e) { if (key === scopeKey.value) { error.value = e.message || '查询选项加载失败'; return } }

+ 0 - 0
src/views/subSystem/drainage/alarm/analysis.vue → src/views/subSystem/alarm/common/analysis.vue


+ 9 - 2
src/views/subSystem/alarm/common/audit.vue

@@ -77,7 +77,13 @@ const reviewVisible = ref(false), detailLoading = ref(false), current = ref(null
 const form = reactive({ auditStatus: undefined, opinion: '' }), formError = ref(''), submitting = ref(false)
 const historyVisible = ref(false), historyLoading = ref(false), historyAlarm = ref(null), historyError = ref('')
 let listRequest = 0, detailRequest = 0, historyRequest = 0
-const scope = computed(() => ({ rootTypeId: Number(route.query.rootTypeId), warningCode: route.query.warning_code || route.query.warningCode || undefined }))
+const scope = computed(() => {
+  const rootTypeId = Number(route.query.rootTypeId)
+  return {
+    rootTypeId: Number.isNaN(rootTypeId) ? undefined : rootTypeId,
+    warningCode: route.query.warning_code || route.query.warningCode || undefined
+  }
+})
 const scopeKey = computed(() => JSON.stringify(scope.value))
 const unreviewed = computed(() => options.auditStatuses.find(item => item.key === 'UNREVIEWED')?.value)
 const reviewChoices = computed(() => options.auditStatuses.filter(item => item.key !== 'UNREVIEWED'))
@@ -101,7 +107,8 @@ async function load() {
   const request = ++listRequest
   loading.value = true; error.value = ''
   try {
-    if (!Number.isInteger(scope.value.rootTypeId) || scope.value.rootTypeId <= 0) throw new Error('菜单缺少有效的 rootTypeId,请检查菜单 query 配置')
+    const rt = scope.value.rootTypeId
+    if (rt !== undefined && (!Number.isInteger(rt) || rt <= 0)) throw new Error('菜单缺少有效的 rootTypeId,请检查菜单 query 配置')
     if (!options.auditStatuses.length) Object.assign(options, (await getAlarmReviewOptions()).data)
     if (request !== listRequest) return
     const [page, count] = await Promise.all([getAlarmReviewPage(params()), getAlarmReviewStatistics(params())])

+ 104 - 0
src/views/subSystem/alarm/common/disposeList.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="alarm-page">
+    <el-card shadow="never" class="alarm-filter-card"><el-form :model="filters" inline class="alarm-filters" @submit.prevent="search">
+      <el-form-item label="报警单号"><el-input v-model="filters.alarmNo" clearable placeholder="请输入报警单号" @keyup.enter="search" /></el-form-item>
+      <el-form-item label="设备搜索"><el-input v-model="filters.keyword" clearable placeholder="请输入设备编码/名称" @keyup.enter="search" /></el-form-item>
+      <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="v in alarmLevels" :key="v.value" :label="v.label" :value="v.value" /></el-select></el-form-item>
+      <el-form-item label="报警时间"><el-date-picker v-model="dates" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" /></el-form-item>
+      <el-form-item><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="reset">重置</el-button></el-form-item>
+    </el-form></el-card>
+    <section class="alarm-table-panel"><div class="alarm-toolbar"><h3>待处理报警</h3><span class="muted">共 {{ total }} 条</span></div>
+      <el-alert v-if="error" :title="error" type="error" show-icon :closable="false" class="load-error"><el-button link @click="load">重新加载</el-button></el-alert>
+      <el-table v-loading="loading" :data="rows" border stripe row-key="id" style="width: 100%" :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" :default-sort="{ prop: 'createTime', order: 'descending' }" @sort-change="sort">
+        <el-table-column prop="alarmNo" label="报警单号" width="210" show-overflow-tooltip><template #default="{ row }">{{ row.alarmNo || '—' }}</template></el-table-column>
+        <el-table-column prop="deviceCode" label="设备编码" width="200" show-overflow-tooltip />
+        <el-table-column prop="deviceName" label="设备名称" min-width="155" show-overflow-tooltip />
+        <el-table-column prop="equipmentLocation" label="安装位置" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="alarmContent" label="报警描述" min-width="270"><template #default="{ row }">{{ row.alarmContent }}</template></el-table-column>
+        <el-table-column label="报警等级" width="105" align="center"><template #default="{ row }"><el-tag :type="levelTone(row.alarmLevel)">{{ alarmLevels.find(item => Number(item.value) === Number(row.alarmLevel))?.label || row.alarmLevelName || '—' }}</el-tag></template></el-table-column>
+        <el-table-column prop="createTime" label="报警时间" width="180" sortable="custom" />
+        <el-table-column label="监测状态" width="100" align="center"><template #default="{ row }"><el-tag :type="row.alarmStatus === 0 ? 'danger' : 'success'">{{ row.monitorStatusName }}</el-tag></template></el-table-column>
+        <el-table-column prop="auditStatusName" label="核实结果" width="105" align="center">
+          <template #default="{ row }"><el-tag effect="light" :type="auditStatusTone(row.auditStatus)">{{ alarmAuditText(row) }}</el-tag></template>
+        </el-table-column>
+        <el-table-column prop="orderStatusName" label="处置状态" width="100" align="center">
+          <template #default="{ row }"><el-tag effect="light" :type="row.workOrderId != null && row.orderStatus != null ? orderStatusType(row.orderStatus) : 'info'">{{ row.orderStatusName || '未转工单' }}</el-tag></template>
+        </el-table-column>
+        <el-table-column prop="receiveUserName" label="处置人" width="100"><template #default="{ row }">{{ row.receiveUserName || '未分配' }}</template></el-table-column>
+        <el-table-column label="操作" width="80" fixed="right" align="center"><template #default="{ row }"><el-button link type="primary" @click="goDispose(row)">处理</el-button></template></el-table-column>
+        <template #empty><el-empty :description="error ? '数据加载失败' : '暂无匹配报警'" /></template>
+      </el-table>
+      <pagination v-show="total > 0" v-model:page="pageNum" v-model:limit="pageSize" :total="total" @pagination="load" />
+    </section>
+  </div>
+</template>
+<script setup name="AlarmDisposeList">
+import { ref, reactive, watch, onBeforeUnmount } from 'vue'
+import { useRouter } from 'vue-router'
+import { Search, Refresh } from '@element-plus/icons-vue'
+import { getAlarms } from '@/api/alarm'
+import { levelTone, auditStatusTone, rangeParams } from './js/alarmOperationsModel'
+import { orderStatusType } from '../../pipeNetwork/ops/workOrder/js/workOrderDict'
+import { useAlarmScope } from './js/useAlarmScope'
+import { alarmAuditText } from '@/utils/alarmLabels'
+import './styles/alarmOperations.scss'
+import { useDict } from '@/utils/dict'
+
+const router = useRouter()
+const { alarm_level: alarmLevels } = useDict('alarm_level')
+const { scope, scopeKey } = useAlarmScope()
+const empty = () => ({ alarmNo: '', keyword: '', alarmLevel: undefined })
+const filters = reactive(empty()), dates = ref([]), rows = ref([]), total = ref(0)
+const pageNum = ref(1), pageSize = ref(10), ascending = ref(false), loading = ref(false), error = ref('')
+
+// 跳转到工单处置页,并带上工单号(orderNo)定位到对应工单,由处置页自动查询。
+// 工单处置菜单路由 path 为 /subSystem/pipeNetwork/ops/pworkOrder/dispose,按后缀(忽略大小写)精确匹配。
+function goDispose(row) {
+  const target = router.getRoutes().find(r => r.path.toLowerCase().endsWith('pworkorder/dispose'))
+  if (target) router.push({ path: target.path, query: { keyword: row.orderNo || '' } })
+}
+
+let generation = 0
+async function load() {
+  const token = ++generation; loading.value = true; error.value = ''
+  // 固定筛选:已核实转工单(audit_status=1) 且 监测未恢复(alarm_status=0)
+  const params = { ...filters, ...rangeParams(dates.value), ...scope.value, auditStatus: 1, alarmStatus: 0 }
+  try {
+    const result = await getAlarms({ ...params, pageNum: pageNum.value, pageSize: pageSize.value, ascending: ascending.value })
+    if (token === generation) { rows.value = result.data.records || []; total.value = result.data.total || 0 }
+  } catch (e) { if (token === generation) { rows.value = []; total.value = 0; error.value = e.message || '加载失败' } }
+  finally { if (token === generation) loading.value = false }
+}
+function search() { pageNum.value = 1; return load() }
+function reset() { Object.assign(filters, empty()); dates.value = []; search() }
+function sort({ order }) { ascending.value = order === 'ascending'; search() }
+watch(scopeKey, async () => {
+  ++generation; rows.value = []; total.value = 0; loading.value = false
+  Object.assign(filters, empty()); dates.value = []
+  await search()
+}, { immediate: true })
+onBeforeUnmount(() => { ++generation })
+</script>
+
+<style scoped lang="scss">
+.alarm-filters {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 12px 24px;
+  margin: 0;
+}
+.alarm-filters :deep(.el-form-item) {
+  margin: 0;
+}
+.alarm-filters :deep(.el-input) {
+  width: 220px;
+}
+.alarm-filters :deep(.el-select) {
+  width: 160px;
+}
+.alarm-filters :deep(.el-date-editor) {
+  width: 300px;
+}
+.alarm-table-panel { padding: 15px 20px 20px; margin-bottom: 20px; background: #fff; border: 1px solid var(--el-card-border-color); border-radius: 8px; }
+</style>

+ 7 - 1
src/views/subSystem/alarm/common/js/alarmOperationsModel.js

@@ -10,7 +10,13 @@ export const levelTone = level => ({ 1: 'danger', 2: 'warning', 3: 'primary', 4:
 export const monitorStatusTone = status => ({ 0: 'danger', 1: 'success' }[status] || 'info')
 export const auditStatusTone = status => ({ 0: 'warning', 1: 'primary', 2: 'info' }[status] || 'info')
 export const levelColor = level => ({ 1: '#f56c6c', 2: '#e6a23c', 3: '#409eff', 4: '#67c23a' }[level] || '#909399')
-export const scopeFromRoute = query => ({ rootTypeId: Number(query.rootTypeId), warningCode: query.warning_code || query.warningCode || undefined })
+export const scopeFromRoute = query => {
+  const rootTypeId = Number(query.rootTypeId)
+  return {
+    rootTypeId: Number.isNaN(rootTypeId) ? undefined : rootTypeId,
+    warningCode: query.warning_code || query.warningCode || undefined
+  }
+}
 export const sampleTime = time => Date.parse(/[zZ]$|[+-]\d\d:\d\d$/.test(time) ? time : String(time).replace(' ', 'T') + '+08:00')
 // 原始采样间隔超过本区间中位间隔的3倍时断线;不补零或插值。
 export function seriesPoints(samples, raw = false) {

+ 2 - 2
src/views/subSystem/lifeCompany/warningAlarmScope.js

@@ -2,8 +2,8 @@
 const alarmMenuComponents = new Set([
   'subSystem/alarm/common/remind',
   'subSystem/alarm/common/list',
-  'subSystem/drainage/alarm/analysis',
-  'subSystem/drainage/alarm/alarmGIS',
+  'subSystem/alarm/common/analysis',
+  'subSystem/alarm/common/alarmGIS',
   'subSystem/waterSupply/alarm/alarmGIS'
 ])
 // EquipmentTypeEnum 的四个设备大类根节点。

+ 2 - 0
src/views/subSystem/pipeNetwork/ops/workOrder/dispose.vue

@@ -1070,6 +1070,8 @@ watch(
   () => route.path,
   (nv, ov) => {
     if (nv === ov) return
+    // 支持从报警处理列表跳转进来时按工单号预填筛选;其余入口不带该 query,行为与之前一致
+    if (route.query?.keyword) queryParams.keyword = String(route.query.keyword)
     queryParams.pageNum = 1
     clearSelection()
     handleSearch()

+ 0 - 15
src/views/subSystem/pipeNetwork/pAlarmMonitor/PEqOperation.vue

@@ -1,15 +0,0 @@
-<!--设备运维管理-->
-<script>
-export default {
-  name: "PEqOperation"
-}
-</script>
-
-<template>
-<div>5.2.4.4. 设备运维管理
-  当本次建设系统接收到前端物联设备的管理系统发送的故障信息时,可实现自动产生故障维护工单,并同时自动把工单发送给对应的维护人员,以此来缩短设备故障响应时间。系统还可生成日常巡检工单以提高日常运维效率;维护人员处理结束后向平台反馈工单处理时间与结果,实现故障维护过程的闭环式管理</div>
-</template>
-
-<style scoped lang="scss">
-
-</style>

+ 0 - 642
src/views/subSystem/pipeNetwork/pAlarmMonitor/PEqWarning.vue

@@ -1,642 +0,0 @@
-<!--预警管理页面(RuoYi Element Plus 标准)-->
-<!--数据源:early_warning 体系 /warning/*接口 -->
-<!--接收后只建工单不派单,由工单系统处理后续流程-->
-
-<template>
-  <div class="warning-container">
-    <!-- ========= 统计卡片区域 ========== -->
-    <el-row :gutter="16" class="stat-row">
-      <el-col :span="4">
-        <div class="stat-item stat-primary">
-          <div class="stat-icon"><el-icon><Warning /></el-icon></div>
-          <div class="stat-content"><div class="stat-title">总预警</div><div class="stat-value">{{ stats.total }}</div></div>
-          <div class="stat-caption">累计预警总量</div>
-        </div>
-      </el-col>
-      <el-col :span="4">
-        <div class="stat-item stat-warning">
-          <div class="stat-icon"><el-icon><Bell /></el-icon></div>
-          <div class="stat-content"><div class="stat-title">待确认</div><div class="stat-value">{{ stats.unconfirmed }}</div></div>
-          <div class="stat-caption">待核实异常</div>
-        </div>
-      </el-col>
-      <el-col :span="4">
-        <div class="stat-item stat-processing">
-          <div class="stat-icon"><el-icon><Tools /></el-icon></div>
-          <div class="stat-content"><div class="stat-title">处理中</div><div class="stat-value">{{ stats.processing }}</div></div>
-          <div class="stat-caption">正在处置</div>
-        </div>
-      </el-col>
-      <el-col :span="4">
-        <div class="stat-item stat-handled">
-          <div class="stat-icon"><el-icon><CircleCheck /></el-icon></div>
-          <div class="stat-content"><div class="stat-title">已处置</div><div class="stat-value">{{ stats.handled }}</div></div>
-          <div class="stat-caption">等待复核清除</div>
-        </div>
-      </el-col>
-      <el-col :span="4">
-        <div class="stat-item stat-closed">
-          <div class="stat-icon"><el-icon><CircleClose /></el-icon></div>
-          <div class="stat-content"><div class="stat-title">已解除</div><div class="stat-value">{{ stats.closed }}</div></div>
-          <div class="stat-caption">已完成闭环</div>
-        </div>
-      </el-col>
-      <el-col :span="4">
-        <div class="stat-item stat-today">
-          <div class="stat-icon"><el-icon><TrendCharts /></el-icon></div>
-          <div class="stat-content"><div class="stat-title">今日新增</div><div class="stat-value">{{ stats.today }}</div></div>
-          <div class="stat-caption">今日新增预警</div>
-        </div>
-      </el-col>
-    </el-row>
-
-    <!-- ========== 图表区域 ========== -->
-    <el-row :gutter="16" class="chart-row">
-      <el-col :span="12"><el-card><template #header><span>近 7 天趋势</span></template><div id="trendChart" style="height:240px"/></el-card></el-col>
-      <el-col :span="6"><el-card><template #header><span>级别分布</span></template><div id="levelChart" style="height:240px"/></el-card></el-col>
-      <el-col :span="6"><el-card><template #header><span>级别占比</span></template><div id="typeChart" style="height:240px"/></el-card></el-col>
-    </el-row>
-
-    <!-- ========= 查询表单 ========== -->
-    <el-card class="search-card">
-      <el-form :inline="true" :model="queryParams">
-        <el-form-item label="预警名称">
-          <el-input v-model="queryParams.warningName" placeholder="请输入预警名称" clearable style="width:200px"/>
-        </el-form-item>
-        <el-form-item label="设备编码">
-          <el-input v-model="queryParams.deviceCode" placeholder="请输入设备编码" clearable style="width:180px"/>
-        </el-form-item>
-        <el-form-item label="设备名称">
-          <el-input v-model="queryParams.deviceName" placeholder="请输入设备名称" clearable style="width:180px"/>
-        </el-form-item>
-        <el-form-item label="状态">
-          <el-select v-model="queryParams.status" placeholder="全部状态" clearable style="width:150px">
-            <el-option label="草稿" value="DRAFT"/>
-            <el-option label="待接收" value="PENDING"/>
-            <el-option label="已接收" value="RECEIVED"/>
-            <el-option label="处理中" value="PROCESSING"/>
-            <el-option label="已处置" value="HANDLED"/>
-            <el-option label="已解除" value="CLOSED"/>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="级别">
-          <el-select v-model="queryParams.warningLevel" placeholder="全部级别" clearable style="width:120px">
-            <el-option label="1 级 (蓝)" value="1"/>
-            <el-option label="2 级 (黄)" value="2"/>
-            <el-option label="3 级 (橙)" value="3"/>
-            <el-option label="4 级 (红)" value="4"/>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="类别">
-          <el-select v-model="queryParams.category" placeholder="全部类别" clearable style="width:120px">
-            <el-option label="普通预警" value="normal"/>
-            <el-option label="特殊预警" value="special"/>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="日期范围">
-          <el-date-picker v-model="queryParams.startDate" type="date" placeholder="开始日期" clearable value-format="YYYY-MM-DD" style="width:160px"/>
-          <span style="margin:0 8px;color:#909399;">至</span>
-          <el-date-picker v-model="queryParams.endDate" type="date" placeholder="结束日期" clearable value-format="YYYY-MM-DD" style="width:160px"/>
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" @click="handleSearch"><el-icon><Search/></el-icon> 查询</el-button>
-          <el-button @click="handleReset"><el-icon><Refresh/></el-icon> 重置</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
-
-    <!-- ========== 预警列表 ========== -->
-    <el-card class="table-card">
-      <el-table :data="tableData" border stripe v-loading="loading" max-height="600">
-        <el-table-column prop="warningNo" label="预警编号" width="140" />
-        <el-table-column prop="warningName" label="预警名称" min-width="160" show-overflow-tooltip />
-        <el-table-column label="关联设备" width="220">
-          <template #default="{ row }">
-            <div v-if="row.deviceCode || row.deviceName" class="device-cell">
-              <span v-if="row.deviceName" class="device-name">{{ row.deviceName }}</span>
-              <el-tag v-if="row.deviceCode" :type="Number(row.warningLevel) === 4 ? 'danger' : 'info'" size="small" effect="plain">{{ row.deviceCode }}</el-tag>
-            </div>
-            <span v-else class="no-device">未关联</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="location" label="设备位置" min-width="140" show-overflow-tooltip />
-        <el-table-column label="类别" width="100">
-          <template #default="{ row }">
-            <el-tag :type="row.category === 'normal' ? '' : 'warning'" size="small">
-              {{ row.category === 'normal' ? '普通预警' : '特殊预警' }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="级别" width="100">
-          <template #default="{ row }">
-            <el-tag :type="getLevelType(row.warningLevel)" size="small">{{ getLevelText(row.warningLevel) }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="状态" width="100">
-          <template #default="{ row }">
-            <el-tag :type="getStatusType(row.status)" size="small">{{ getStatusText(row.status) }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="publishTime" label="发布时间" width="170" sortable class-name="publish-time-column" :formatter="formatPublishTime" />
-        <el-table-column label="操作" width="180" fixed="right">
-          <template #default="{ row }">
-            <el-button v-if="isPending(row.status)" v-hasPermi="['warning:warning:handle']" type="primary" size="small" @click="openConfirm(row)">接收</el-button>
-            <el-button v-if="isReceived(row.status)" v-hasPermi="['warning:warning:handle']" type="primary" size="small" @click="openStart(row)">开始处置</el-button>
-            <el-button v-if="row.status === 'PROCESSING'" v-hasPermi="['warning:warning:handle']" type="warning" size="small" @click="openProcess(row)">提交处理</el-button>
-            <WarningReviewAction :row="row" @changed="loadList" />
-            <el-button type="info" size="small" @click="openDetail(row.warningId)">详情</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total" layout="total,prev,pager,next,sizes" :page-sizes="[10,20,30]"
-                     @change="loadList" style="margin-top:16px;justify-content:flex-end" />
-    </el-card>
-
-    <!-- ========== 接收预警弹窗 ========== -->
-    <el-dialog v-model="confirmVisible" title="接收预警" width="600px" append-to-body destroy-on-close>
-      <el-descriptions :column="1" border>
-        <el-descriptions-item label="预警编号">{{ current.warningNo }}</el-descriptions-item>
-        <el-descriptions-item label="预警名称">{{ current.warningName }}</el-descriptions-item>
-        <el-descriptions-item label="预警内容">{{ current.warningContent || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="关联设备">{{ formatWarningDevice(current) }}</el-descriptions-item>
-        <el-descriptions-item label="设备位置">{{ current.location || '-' }}</el-descriptions-item>
-      </el-descriptions>
-      <el-form label-width="100px" style="margin-top:16px">
-        <el-form-item label="备注信息">
-          <el-input v-model="confirmingRemark" type="textarea" :rows="3" placeholder="填写异常情况描述(将写入工单描述)" maxlength="300" show-word-limit/>
-        </el-form-item>
-        <el-form-item label="处理人员">
-          <el-input v-model="assignedUser" placeholder="可填写处理人账号,不填则使用预警原处理人或当前用户" clearable />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="confirmVisible=false">取消</el-button>
-        <el-button type="primary" :loading="submitting" @click="submitConfirm">确认提交</el-button>
-      </template>
-    </el-dialog>
-
-    <el-dialog v-model="startVisible" title="开始处置" width="560px" append-to-body destroy-on-close>
-      <el-descriptions :column="1" border>
-        <el-descriptions-item label="预警编号">{{ startRow.warningNo }}</el-descriptions-item>
-        <el-descriptions-item label="预警名称">{{ startRow.warningName }}</el-descriptions-item>
-        <el-descriptions-item label="关联设备">{{ formatWarningDevice(startRow) }}</el-descriptions-item>
-      </el-descriptions>
-      <el-form label-width="100px" style="margin-top:16px">
-        <el-form-item label="处置说明">
-          <el-input v-model="startRemark" type="textarea" :rows="3" placeholder="填写开始处置的说明(可选)" maxlength="300" show-word-limit/>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="startVisible=false">取消</el-button>
-        <el-button type="primary" :loading="submitting" @click="submitStart">确认开始</el-button>
-      </template>
-    </el-dialog>
-
-    <el-dialog v-model="processVisible" title="提交预警处理反馈" width="620px" append-to-body destroy-on-close>
-      <el-descriptions :column="1" border>
-        <el-descriptions-item label="预警编号">{{ processRow.warningNo }}</el-descriptions-item>
-        <el-descriptions-item label="预警名称">{{ processRow.warningName }}</el-descriptions-item>
-        <el-descriptions-item label="关联设备">{{ formatWarningDevice(processRow) }}</el-descriptions-item>
-      </el-descriptions>
-      <el-form label-width="100px" style="margin-top:16px">
-        <el-form-item label="处理反馈" required>
-          <el-input v-model="processContent" type="textarea" :rows="5" maxlength="1000" show-word-limit placeholder="请填写处理过程和处理结果" />
-        </el-form-item>
-        <el-form-item label="处理附件">
-          <el-upload v-model:file-list="processFileList" action="#" list-type="text" :auto-upload="false" :limit="9" accept=".jpg,.jpeg,.png,.gif,.webp,.pdf,.xls,.xlsx,.doc,.docx" :on-change="handleProcessFileChange" :on-remove="handleProcessFileRemove" :on-exceed="handleProcessFileExceed">
-            <el-icon><Plus /></el-icon>
-          </el-upload>
-          <div class="upload-tip">支持图片、PDF、Excel、Word,最多 9 个文件,单个不超过 20MB</div>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="processVisible=false">取消</el-button>
-        <el-button type="primary" :loading="submitting" @click="submitProcess">提交反馈</el-button>
-      </template>
-    </el-dialog>
-
-    <WarningDetailDrawer v-model="detailVisible" :warning-id="selectedDetailWarningId" />
-  </div>
-</template>
-
-<script setup>
-import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue'
-import * as echarts from 'echarts'
-import { ElMessage } from 'element-plus'
-import { Search, Refresh, Plus, Warning, Bell, Tools, CircleCheck, CircleClose, TrendCharts } from '@element-plus/icons-vue'
-import { getWarningDashboardStatisticsData, getWarningSearchData, confirmWarningData, startWarningProcessData, submitWarningProcessData } from '@/api/pipeNetwork/earlyWarning'
-import WarningDetailDrawer from '@/views/subSystem/lifeCompany/components/WarningDetailDrawer.vue'
-import WarningReviewAction from '@/views/subSystem/lifeCompany/components/WarningReviewAction.vue'
-import { addManholeWorkOrder } from '@/api/pipeNetwork/basic'
-
-// ========== 统计数据 ==========
-const stats = ref({ total:0, unconfirmed:0, processing:0, handled:0, closed:0, today:0 })
-
-// ========== 图表数据 ==========
-const chartData = ref({ trend:[], levels:[], types:[] })
-
-// ========== 查询参数 & 列表 ==========
-const queryParams = ref({ warningName:'', deviceCode:'', deviceName:'', status:'', warningLevel:'', category:'', startDate:'', endDate:'' })
-const tableData = ref([])
-const loading = ref(false)
-const pageNum = ref(1)
-const pageSize = ref(10)
-const total = ref(0)
-
-async function loadStats() {
-  try {
-    const res = await getWarningDashboardStatisticsData()
-    if (!res.data) return
-    const b = res.data.basicStats
-    const t = res.data.trendStats || []
-    chartData.value.trend = t.map(item => ({
-      date: String(item.date || '').slice(5) || '-',
-      count: Number(item.count || 0)
-    }))
-    chartData.value.levels = (res.data.levelStats || []).map(item => ({
-      levelCode: String(item.levelCode || ''),
-      count: Number(item.count || 0)
-    }))
-    chartData.value.types = (res.data.typeStats || []).map(item => ({
-      typeCode: item.typeCode,
-      typeName: item.typeName || item.typeCode,
-      count: Number(item.count || 0)
-    }))
-    // releasedCount + pendingCount = 待确认
-    stats.value = {
-      total: Number(b.total || 0),
-      unconfirmed: Number(b.releasedCount || 0) + Number(b.pendingCount || 0),
-      processing: Number(b.processingCount || 0),
-      handled: Number(b.handledCount || 0),
-      closed: Number(b.closedCount || 0),
-      today: t.length ? Number(t[t.length-1].count || 0) : 0
-    }
-  } catch(e) { console.warn('加载统计数据失败', e) }
-}
-
-async function loadList() {
-  loading.value = true
-  try {
-    const params = { pageNum: pageNum.value, pageSize: pageSize.value }
-    if (queryParams.value.warningName) params.warningName = queryParams.value.warningName
-    if (queryParams.value.deviceCode) params.deviceCode = queryParams.value.deviceCode
-    if (queryParams.value.deviceName) params.deviceName = queryParams.value.deviceName
-    if (queryParams.value.status) params.status = queryParams.value.status
-    if (queryParams.value.warningLevel) params.warningLevel = queryParams.value.warningLevel
-    if (queryParams.value.category === 'normal') params.publisher = '系统自动'
-    if (queryParams.value.category === 'special') params.excludePublisher = '系统自动'
-    if (queryParams.value.startDate) params.startTime = queryParams.value.startDate + ' 00:00:00'
-    if (queryParams.value.endDate) params.endTime = queryParams.value.endDate + ' 23:59:59'
-    
-    const res = await getWarningSearchData(params)
-    if (!res.data) return
-    tableData.value = (res.data.records || []).map(row => normalizeWarning(row))
-    total.value = Number(res.data.total || 0)
-  } catch(e) { ElMessage.error('加载预警列表失败'); console.error(e) }
-  finally { loading.value = false }
-}
-
-// 后端统计接口异常或没有配置数据时,仍显示稳定的默认图表状态
-const fillChartData = () => {
-  if (chartData.value.trend?.length && chartData.value.levels?.length && chartData.value.types?.length) return
-  const dates = [], counts = []
-  for(let i=6;i>=0;i--) { const d=new Date(); d.setDate(d.getDate()-i); dates.push((d.getMonth()+1)+'-'+d.getDate()) }
-  for(let i=6;i>=0;i--) { counts.push(0) }
-  if (!chartData.value.trend?.length) chartData.value.trend = dates.map((date,idx)=>({date,count:counts[idx]}))
-  if (!chartData.value.levels?.length) chartData.value.levels = [1,2,3,4].map(lv=>({levelCode:String(lv), count:0}))
-  if (!chartData.value.types?.length) chartData.value.types = [{typeCode:'empty',typeName:'暂无数据',count:1}]
-}
-
-// 图表渲染时的空数据状态
-const emptyChartData = { trend:[], levels:[], types:[] }
-
-// ========== 图表管理 ==========
-let trendChart, levelChart, typeChart
-
-const initCharts = () => {
-  // 如果数据为空,填充模拟数据
-  if (chartData.value.trend?.length === 0) fillChartData()
-  try { renderTrendChart() } catch(e){ console.warn('趋势图初始化失败:', e) }
-  try { renderLevelChart() } catch(e){ console.warn('级别图初始化失败:', e) }
-  try { renderTypeChart() } catch(e){ console.warn('类型图初始化失败:', e) }
-}
-
-// 下划线转驼峰 + 判断类别
-function normalizeWarning(row) {
-  const n = {}
-  Object.keys(row).forEach(key => {
-    const camel = key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
-    n[camel] = row[key]
-  })
-  n.category = n.publisher === '系统自动' ? 'normal' : 'special'
-  // 从 remark 字段提取设备编码 (AUTO|deviceCode|warningCode)
-  n.deviceCode = n.deviceCode || extractDeviceCodeStr(row)
-  n.deviceName = n.deviceName || row.device_name || row.deviceName || ''
-  return n
-}
-
-function formatPublishTime(row, column, value) {
-  return formatWarningTime(value)
-}
-
-function formatWarningTime(value) {
-  if (!value) return '-'
-  const text = String(value).trim()
-  const match = text.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})/)
-  if (match) return `${match[1]} ${match[2]}`
-  return text.replace('T', ' ').replace(/\.\d+(Z|[+-]\d{2}:?\d{2})$/, '')
-}
-
-function formatWarningDevice(warning) {
-  const name = warning?.deviceName || ''
-  const code = warning?.deviceCode || ''
-  if (name && code) return `${name}(${code})`
-  return name || code || '未关联'
-}
-
-// 从预警行中提取设备编码
-function extractDeviceCodeStr(row) {
-  if (row.remark && typeof row.remark === 'string' && row.remark.startsWith('AUTO|')) {
-    const parts = row.remark.split('|')
-    if (parts.length >= 2) return parts[1]
-  }
-  const m = (row.warning_content || row.warningContent || '').match(/设备\s*\[([^\]]+)\]/)
-  if (m) return m[1]
-  return ''
-}
-
-// ========== ECharts ==========
-const trendChartOpt = {
-  tooltip:{ trigger:'axis' },
-  legend:{ data:['新增预警'], top:0 },
-  grid:{ left:40,right:20,top:44,bottom:32 },
-  xAxis:{ type:'category', axisLabel:{ color:'#9098a6' }, axisLine:{ lineStyle:{ color:'#d0d5dd' } } },
-  yAxis:{ type:'value', name:'数量', axisLabel:{ color:'#9098a6' }, splitLine:{ lineStyle:{ color:'#f0f2f6',type:'dashed' } } },
-  series:[{ type:'line', smooth:true, data:[], lineStyle:{ color:'#e74c3c',width:2 }, itemStyle:{ color:'#e74c3c' } }]
-}
-
-const levelChartOpt = {
-  tooltip:{ trigger:'axis' },
-  grid:{ left:40,right:20,top:44,bottom:32 },
-  xAxis:{ type:'category', data: ['1 级','2 级','3 级','4 级'], axisLabel:{ color:'#9098a6' }, axisLine:{ lineStyle:{ color:'#d0d5dd' } } },
-  yAxis:{ type:'value', name:'数量', axisLabel:{ color:'#9098a6' }, splitLine:{ lineStyle:{ color:'#f0f2f6',type:'dashed' } } },
-  series:[{
-    type:'bar', barWidth:34,
-    label:{ show:true, position:'top', color:'#606266', fontSize:12 },
-    data:[],
-    itemStyle:{
-      color:function(params){
-        const colors={1:'#5b8ff9',2:'#f6bd16',3:'#f6903d',4:'#e8684a'};
-        return colors[params.dataIndex+1]||'#e3f2fd';
-      },
-      borderRadius:[4,4,0,0]
-    }
-  }]
-}
-
-const typeChartOpt = {
-  tooltip:{ trigger:'item' },
-  legend:{ top:'center', orient:'vertical', right:4, itemWidth:12, itemHeight:12, textStyle:{ color:'#606266' } },
-  series:[{
-    type:'pie', radius:['40%','70%'], avoidLabelOverlap:false,
-    itemStyle:{ borderRadius:4, borderColor:'#fff', borderWidth:1 },
-    label:{ show:false },
-    data:[]
-  }]
-}
-
-const resizeCharts = () => { trendChart?.resize(); levelChart?.resize(); typeChart?.resize() }
-
-// ========== 图表渲染函数 ==========
-function renderTrendChart() {
-  const dom = document.getElementById('trendChart')
-  if (!dom) return
-  if (trendChart) trendChart.dispose()
-  trendChart = echarts.init(dom)
-  const data = chartData.value.trend.length ? chartData.value.trend : emptyChartData.trend
-  trendChart.setOption({ ...trendChartOpt, xAxis:{ ...trendChartOpt.xAxis, data: data.map(i=>i.date) }, series:[{ ...trendChartOpt.series[0], data: data.map(i=>i.count || 0) }]
-  })
-}
-
-function renderLevelChart() {
-  const dom = document.getElementById('levelChart')
-  if (!dom) return
-  if (levelChart) levelChart.dispose()
-  levelChart = echarts.init(dom)
-  const data = chartData.value.levels.length ? chartData.value.levels : emptyChartData.levels
-  levelChart.setOption({ ...levelChartOpt, series:[{ ...levelChartOpt.series[0], data: data.map(l=>l.count || 0) }]
-  })
-}
-
-function renderTypeChart() {
-  const dom = document.getElementById('typeChart')
-  if (!dom) return
-  if (typeChart) typeChart.dispose()
-  typeChart = echarts.init(dom)
-  const levels = chartData.value.levels || []
-  const levelNames = { '1':'1 级(蓝)', '2':'2 级(黄)', '3':'3 级(橙)', '4':'4 级(红)' }
-  const total = levels.reduce((sum, item) => sum + Number(item.count || 0), 0)
-  const data = total > 0
-    ? levels.map(item => ({ value:Number(item.count || 0), name:levelNames[String(item.levelCode)] || `${item.levelCode} 级` }))
-    : [{ value:1, name:'暂无数据', itemStyle:{ color:'#c0c4cc' } }]
-  typeChart.setOption({ ...typeChartOpt, series:[{ ...typeChartOpt.series[0], data }]
-  })
-}
-
-// ========== 辅助函数 ==========
-const isPending = (status) => status === 'PENDING'
-const isReceived = (status) => status === 'RECEIVED'
-const getStatusText = (s) => ({ DRAFT:'草稿', PENDING:'待接收', RECEIVED:'已接收', PROCESSING:'处置中', HANDLED:'已处置', CLOSED:'已解除' }[s]||s)
-const getStatusType = (s) => ({ DRAFT:'info', PENDING:'danger', RECEIVED:'warning', PROCESSING:'warning', HANDLED:'primary', CLOSED:'success' }[s]||'')
-const getLevelText = (lv) => ({ '1':'1 级 (蓝)','2':'2 级 (黄)','3':'3 级 (橙)','4':'4 级 (红)' }[lv]||'-' )
-const getLevelType = (lv) => ({ '1':'','2':'warning','3':'warning','4':'danger' }[lv]||'')
-// ========== 操作处理 ==========
-function handleSearch() { pageNum.value=1; loadList() }
-function handleReset() { queryParams.value={warningName:'',deviceCode:'',deviceName:'',status:'',warningLevel:'',category:'',startDate:'',endDate:''}; pageNum.value=1; loadList() }
-
-// ========== 接收预警 ==========
-const confirmVisible = ref(false)
-const current = ref({})
-const confirmingRemark = ref('')
-const assignedUser = ref('')
-const submitting = ref(false)
-
-function openConfirm(row) {
-  current.value = row
-  confirmingRemark.value = ''
-  assignedUser.value = row.assignedUser || row.handler || ''
-  confirmVisible.value = true
-}
-
-async function submitConfirm() {
-  if (!confirmingRemark.value.trim()) { ElMessage.warning('请填写接收说明'); return }
-  
-  submitting.value = true
-  try {
-    await confirmWarningData({ warningId: current.value.warningId, assignedUser: assignedUser.value, confirmRemark: confirmingRemark.value })
-    const orderDesc = `预警「${current.value.warningName}」已接收;${confirmingRemark.value}`
-    await addManholeWorkOrder({ alarmId: current.value.warningId, orderLevel: getWorkOrderLevel(current.value.warningLevel), orderDesc: orderDesc.slice(0,500) })
-    ElMessage.success('预警已接收')
-    confirmVisible.value = false
-    loadList()
-  } catch(e) { ElMessage.error('接收失败:'+(e.msg||e.message||'重试')); console.error(e) }
-  finally { submitting.value=false }
-}
-
-function getWorkOrderLevel(warningLevel) { const lv = Number(warningLevel); return lv>=3?1:(lv===2?2:3) }
-
-// ========== 开始处置 ==========
-const startVisible = ref(false)
-const startRow = ref({})
-const startRemark = ref('')
-
-function openStart(row) {
-  startRow.value = row
-  startRemark.value = ''
-  startVisible.value = true
-}
-
-async function submitStart() {
-  submitting.value = true
-  try {
-    await startWarningProcessData({ warningId: startRow.value.warningId, remark: startRemark.value })
-    ElMessage.success('已开始处置')
-    startVisible.value = false
-    loadList()
-  } catch(e) { ElMessage.error('开始处置失败:'+(e.msg||e.message||'重试')); console.error(e) }
-  finally { submitting.value=false }
-}
-
-const processVisible = ref(false)
-const processRow = ref({})
-const processContent = ref('')
-const processFileList = ref([])
-const processFiles = ref([])
-
-function openProcess(row) {
-  processRow.value = row
-  processContent.value = ''
-  processFiles.value = []
-  processFileList.value = []
-  processVisible.value = true
-}
-
-function handleProcessFileChange(uploadFile, uploadFiles) {
-  const raw = uploadFile.raw
-  const extension = String(raw?.name || '').split('.').pop().toLowerCase()
-  const allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'xls', 'xlsx', 'doc', 'docx']
-  if (!raw || !allowedExtensions.includes(extension)) {
-    processFileList.value = uploadFiles.filter(item => item.uid !== uploadFile.uid)
-    ElMessage.warning('仅支持图片、PDF、Excel、Word文件')
-    return
-  }
-  if (raw.size > 20 * 1024 * 1024) {
-    processFileList.value = uploadFiles.filter(item => item.uid !== uploadFile.uid)
-    ElMessage.warning('单个附件不能超过20MB')
-    return
-  }
-  processFileList.value = uploadFiles
-  processFiles.value = uploadFiles.map(item => item.raw).filter(Boolean)
-}
-
-function handleProcessFileRemove(uploadFile, uploadFiles) {
-  handleProcessFileChange(uploadFile, uploadFiles)
-}
-
-function handleProcessFileExceed() {
-  ElMessage.warning('最多上传9个附件')
-}
-
-async function submitProcess() {
-  if (!processContent.value.trim()) {
-    ElMessage.warning('请填写处理反馈')
-    return
-  }
-  submitting.value = true
-  try {
-    await submitWarningProcessData({ warningId: processRow.value.warningId, processContent: processContent.value, files: processFiles.value })
-    ElMessage.success('处理反馈已提交')
-    processVisible.value = false
-    await loadList()
-  } catch (e) {
-    ElMessage.error('提交处理反馈失败')
-  } finally {
-    submitting.value = false
-  }
-}
-
-// ========== 详情抽屉 ==========
-const detailVisible = ref(false)
-const selectedDetailWarningId = ref('')
-
-function openDetail(warningId) {
-  selectedDetailWarningId.value = warningId
-  detailVisible.value = true
-}
-
-onBeforeUnmount(() => {
-  window.removeEventListener('resize', resizeCharts)
-  trendChart?.dispose()
-  levelChart?.dispose()
-  typeChart?.dispose()
-})
-
-onMounted(async () => {
-  await nextTick()
-  await Promise.all([loadStats(), loadList()])
-  await fillChartData()
-  initCharts()
-  window.addEventListener('resize', resizeCharts)
-})
-</script>
-
-<style scoped>
-.warning-container { padding: 20px; min-height: calc(100vh - 84px); }
-.stat-row { margin-bottom: 16px; }
-.stat-item {
-  position:relative;min-height:104px;padding:16px 16px 12px 62px;background:#fff;
-  border:1px solid #e9edf3;border-radius:10px;box-shadow:0 3px 12px rgba(31,45,61,.07);
-  overflow:hidden;transition:transform .2s,box-shadow .2s;
-}
-.stat-item::before { content:'';position:absolute;left:0;top:0;bottom:0;width:4px;background:var(--stat-color); }
-.stat-item:hover { transform:translateY(-2px);box-shadow:0 8px 20px rgba(31,45,61,.12); }
-.stat-icon { position:absolute;left:16px;top:22px;width:34px;height:34px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--stat-color);background:var(--stat-bg);font-size:19px; }
-.stat-content { display:flex;align-items:baseline;justify-content:space-between;gap:8px; }
-.stat-title { font-size:13px;color:#667085;white-space:nowrap; }
-.stat-value { font-size:30px;line-height:34px;font-weight:700;color:var(--stat-color);transition:all .3s; }
-.stat-caption { margin-top:8px;color:#98a2b3;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
-.stat-primary { --stat-color:#409eff;--stat-bg:#ecf5ff; }
-.stat-warning { --stat-color:#e6a23c;--stat-bg:#fdf6ec; }
-.stat-processing { --stat-color:#8a96a3;--stat-bg:#f2f4f7; }
-.stat-handled { --stat-color:#67c23a;--stat-bg:#f0f9eb; }
-.stat-closed { --stat-color:#18a567;--stat-bg:#eafaf3; }
-.stat-today { --stat-color:#8e5cc7;--stat-bg:#f5effb; }
-.stat-value.highlight { animation: pulse 1.5s ease-in-out; }
-@keyframes pulse { 0%{transform:scale(1)}50%{transform:scale(1.12)}100%{transform:scale(1)} }
-
-.chart-row { margin-bottom: 16px; }
-.chart-card { min-height: 300px; }
-.chart-empty {
-  display: flex; align-items: center; justify-content: center;
-  height: 240px; color: #c0c4cc; font-size: 14px;
-  flex-direction: column; gap: 8px;
-}
-.chart-empty::before { content:'\1F4CA'; font-size: 32px; opacity: 0.5; }
-
-.search-card { margin-bottom: 16px; }
-.table-card { min-height: 600px; }
-.device-cell { display:flex; align-items:center; gap:4px; }
-.device-name { max-width:96px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#606266; font-size:12px; }
-.no-device { color:#c0c4cc; font-size:12px; }
-:deep(.publish-time-column .cell) { white-space: nowrap; }
-
-.upload-tip { color:#909399;font-size:12px;line-height:20px }
-
-@media (max-width: 1300px) {
-  .stat-row { display:flex; gap:10px; flex-wrap:wrap; }
-  .stat-row > * { flex-shrink:0; width: calc(33.33% - 14px) !important; }
-}
-@media (max-width: 900px) {
-  .stat-row > * { width: calc(50% - 10px) !important; }
-}
-</style>