瀏覽代碼

fix:燃气管网-压力设备告警与可燃气体告警

null 4 天之前
父節點
當前提交
35e2b15fe6

+ 35 - 2
src/api/pipeNetwork/basic.js

@@ -124,6 +124,33 @@ export function operManholeWorkOrder(data) {
 export function getRepairRecordPage(pageNum, pageSize, params) {
   return request({ url: '/api/repair-record/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
 }
+export function getRepairStats() {
+  return request({ url: '/api/repair-record/stats', method: 'get' })
+}
+
+// 可燃气体报警
+export function getGasAlarmEquipment() {
+  return request({ url: '/api/gas-alarm/equipment', method: 'get' })
+}
+export function getGasAlarmHistory(pageNum, pageSize, startDate, endDate, deviceCode) {
+  return request({ url: '/api/gas-alarm/history', method: 'get', params: { pageNum, pageSize, startDate, endDate, deviceCode } })
+}
+export function getGasAlarmStats() {
+  return request({ url: '/api/gas-alarm/stats', method: 'get' })
+}
+export function resolveGasAlarm(id) {
+  return request({ url: '/api/gas-alarm/resolve/' + id, method: 'put' })
+}
+export function dispatchGasAlarm(id) {
+  return request({ url: '/api/gas-alarm/dispatch/' + id, method: 'post' })
+}
+
+// 管线压力报警
+export function getPressureAlarmEquipment() { return request({ url: '/api/pressure-alarm/equipment', method: 'get' }) }
+export function getPressureAlarmHistory(pageNum, pageSize, startDate, endDate, deviceCode) { return request({ url: '/api/pressure-alarm/history', method: 'get', params: { pageNum, pageSize, startDate, endDate, deviceCode } }) }
+export function getPressureAlarmStats() { return request({ url: '/api/pressure-alarm/stats', method: 'get' }) }
+export function resolvePressureAlarm(id) { return request({ url: '/api/pressure-alarm/resolve/' + id, method: 'put' }) }
+export function dispatchPressureAlarm(id) { return request({ url: '/api/pressure-alarm/dispatch/' + id, method: 'post' }) }
 export function getRepairRecordById(id) {
   return request({ url: '/api/repair-record/getById/' + id, method: 'get' })
 }
@@ -154,10 +181,16 @@ export function getEquipmentById(id) {
   return request({ url: '/EquipmentBase/getById/' + id, method: 'get' })
 }
 export function addEquipment(data) {
-  return request({ url: '/api/equipment/save', method: 'post', data })
+  const params = {}
+  if (data.networkId) params.networkId = data.networkId
+  if (data.pointId) params.pointId = data.pointId
+  return request({ url: '/api/equipment/save', method: 'post', data, params })
 }
 export function updateEquipment(data) {
-  return request({ url: '/api/equipment/updateById', method: 'put', data })
+  const params = {}
+  if (data.networkId) params.networkId = data.networkId
+  if (data.pointId) params.pointId = data.pointId
+  return request({ url: '/api/equipment/updateById', method: 'put', data, params })
 }
 export function deleteEquipment(id) {
   return request({ url: '/api/equipment/deleteById', method: 'delete', params: { id } })

+ 13 - 9
src/views/subSystem/basic/Equipment.vue

@@ -219,7 +219,7 @@
 <script setup>
 import { ref, reactive, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { getEquipmentFullPage, addEquipment, updateEquipment, deleteEquipment, getEquipmentTypeList, getPipeNetworkOptions, getGasPipePointPage, saveEquipmentDeviceRel } from '@/api/pipeNetwork/basic'
+import { getEquipmentFullPage, addEquipment, updateEquipment, deleteEquipment, getEquipmentTypeList, getPipeNetworkOptions, getGasPipePointPage } from '@/api/pipeNetwork/basic'
 
 const loading = ref(false)
 const tableData = ref([])
@@ -241,7 +241,7 @@ const equipmentTypeOptions = ref([])
 const networkOptions = ref([])
 const pointOptions = ref([])
 const formData = reactive({
-  equipmentId: '', equipmentCode: '', equipmentName: '', equipmentModel: '',
+  equipmentCode: '', equipmentName: '', equipmentModel: '',
   equipmentSpec: '', manufacturer: '', equipmentLocation: '', equipmentTypeId: '',
   networkId: '', pointId: '',
   longitude: null, latitude: null, maintainer: '', maintainerPhone: '', remark: ''
@@ -285,7 +285,9 @@ function onNetworkChange(val) {
 }
 function handleAdd() {
   dialogTitle.value = '新增'
-  Object.assign(formData, { equipmentId: '', equipmentCode: '', equipmentName: '', equipmentModel: '', equipmentSpec: '', manufacturer: '', equipmentLocation: '', equipmentTypeId: '', networkId: '', pointId: '', longitude: null, latitude: null, maintainer: '', maintainerPhone: '', remark: '' })
+  const template = { equipmentCode: '', equipmentName: '', equipmentModel: '', equipmentSpec: '', manufacturer: '', equipmentLocation: '', equipmentTypeId: '', networkId: '', pointId: '', longitude: null, latitude: null, maintainer: '', maintainerPhone: '', remark: '' }
+  if (formData.equipmentId) delete formData.equipmentId
+  Object.assign(formData, template)
   dialogVisible.value = true
   loadEquipmentTypes(); loadNetworkOptions()
 }
@@ -300,16 +302,18 @@ async function handleSave() {
   const valid = await formRef.value.validate().catch(() => false)
   if (!valid) return
   try {
-    if (formData.equipmentId) {
+    const isEdit = !!formData.equipmentId
+    if (isEdit) {
       await updateEquipment(formData)
     } else {
       const res = await addEquipment(formData)
-      formData.equipmentId = res.data
+      formData.equipmentId = res.data // 后端 saveWithCheck 返回实体 UUID
+      if (!formData.equipmentId) {
+        ElMessage.error('新增失败,未获取到设备ID')
+        return
+      }
     }
-    if (formData.pointId && formData.equipmentId) {
-      await saveEquipmentDeviceRel({ equipmentId: formData.equipmentId, pointId: formData.pointId }).catch(() => {})
-    }
-    ElMessage.success(formData.equipmentId ? '修改成功' : '新增成功')
+    ElMessage.success(isEdit ? '修改成功' : '新增成功')
     dialogVisible.value = false; loadData()
   } catch (e) {
     const msg = e?.response?.data?.msg || e?.message || '操作失败'

+ 1 - 1
src/views/subSystem/basic/EquipmentAbnormal.vue

@@ -58,7 +58,7 @@
         <div class="type-list">
           <div class="type-item" v-for="item in deviceTypeStats" :key="item.name">
             <span class="type-name">{{ item.name }}</span>
-            <span class="type-bar-bg"><span class="type-bar" :style="{width: (item.count/stats.total*100)+'%', background: item.color}"></span></span>
+            <span class="type-bar-bg"><span class="type-bar" :style="{width: (stats.total > 0 ? item.count/stats.total*100 : 0)+'%', background: item.color}"></span></span>
             <span class="type-count">{{ item.count }}</span>
           </div>
         </div>

+ 32 - 13
src/views/subSystem/basic/PipeNetwork.vue

@@ -13,11 +13,6 @@
             <el-option v-for="d in pipeNetworkLevelDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
           </el-select>
         </el-form-item>
-        <el-form-item label="设施类型">
-          <el-select v-model="searchForm.networkType" placeholder="请选择" clearable style="width:160px">
-            <el-option v-for="d in pipeNetworkTypeDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
-          </el-select>
-        </el-form-item>
         <el-form-item label="材质">
           <el-input v-model="searchForm.material" placeholder="请输入材质" clearable style="width:160px" />
         </el-form-item>
@@ -47,7 +42,10 @@
         </el-table-column>
         <el-table-column prop="managementUnit" label="管养单位" min-width="150" />
         <el-table-column prop="material" label="材质" width="100" />
+        <el-table-column prop="length" label="长度(米)" width="100" />
+        <el-table-column prop="diameter" label="管径(毫米)" width="110" />
         <el-table-column prop="location" label="位置" min-width="180" />
+        <el-table-column prop="buildDate" label="建设日期" width="120" />
         <el-table-column prop="status" label="状态" width="90">
           <template #default="{ row }">
             <el-tag :type="row.status === '0' ? 'success' : 'danger'">{{ getDictLabel(pipeNetworkStatusDict, row.status) }}</el-tag>
@@ -79,11 +77,6 @@
         <el-form-item label="设施名称" prop="networkName">
           <el-input v-model="formData.networkName" placeholder="请输入设施名称" />
         </el-form-item>
-        <el-form-item label="设施类型">
-          <el-select v-model="formData.networkType">
-            <el-option v-for="d in pipeNetworkTypeDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
-          </el-select>
-        </el-form-item>
         <el-form-item label="等级">
           <el-select v-model="formData.networkLevel">
             <el-option v-for="d in pipeNetworkLevelDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
@@ -98,6 +91,15 @@
         <el-form-item label="材质">
           <el-input v-model="formData.material" placeholder="请输入材质" />
         </el-form-item>
+        <el-form-item label="长度(米)">
+          <el-input-number v-model="formData.length" :precision="2" :step="1" :min="0" placeholder="请输入长度" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="管径(毫米)">
+          <el-input-number v-model="formData.diameter" :precision="0" :step="1" :min="0" placeholder="请输入管径" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="建设日期">
+          <el-date-picker v-model="formData.buildDate" type="date" placeholder="请选择建设日期" value-format="YYYY-MM-DD" style="width: 100%" />
+        </el-form-item>
         <el-form-item label="经度">
           <el-input-number v-model="formData.longitude" :precision="10" :step="0.0001" placeholder="请输入经度" style="width: 100%" />
         </el-form-item>
@@ -142,8 +144,9 @@ const dialogVisible = ref(false)
 const dialogTitle = ref('新增')
 const formRef = ref(null)
 const formData = reactive({
-  networkId: '', networkCode: '', networkName: '', networkType: '0',
+  networkId: '', networkCode: '', networkName: '', networkType: 'GAS',
   networkLevel: '', managementUnit: '', location: '', material: '',
+  length: null, diameter: null, buildDate: null,
   longitude: null, latitude: null, status: '0', remark: ''
 })
 
@@ -197,13 +200,29 @@ function handleReset() {
 
 function handleAdd() {
   dialogTitle.value = '新增'
-  Object.assign(formData, { networkId: '', networkCode: '', networkName: '', networkType: '0', networkLevel: '', managementUnit: '', location: '', material: '', longitude: null, latitude: null, status: '0', remark: '' })
+  Object.assign(formData, { networkId: '', networkCode: '', networkName: '', networkType: 'GAS', networkLevel: '', managementUnit: '', location: '', material: '', length: null, diameter: null, buildDate: null, longitude: null, latitude: null, status: '0', remark: '' })
   dialogVisible.value = true
 }
 
+function toDateStr(val) {
+  // 转为 yyyy-MM-dd 字符串,供 value-format="YYYY-MM-DD" 的 date-picker 回显
+  if (!val) return ''
+  if (Array.isArray(val)) {
+    return val[0] + '-' + String(val[1]).padStart(2, '0') + '-' + String(val[2]).padStart(2, '0')
+  }
+  const s = String(val)
+  const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/)
+  if (m) return m[0]
+  const d = new Date(s)
+  if (!isNaN(d.getTime())) {
+    return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
+  }
+  return ''
+}
+
 function handleEdit(row) {
   dialogTitle.value = '编辑'
-  Object.assign(formData, row)
+  Object.assign(formData, { ...row, buildDate: toDateStr(row.buildDate), networkType: row.networkType || 'GAS' })
   dialogVisible.value = true
 }
 

+ 9 - 2
src/views/subSystem/basic/PointManage.vue

@@ -25,7 +25,9 @@
         <el-table-column prop="pointType" label="测点类型" width="110">
           <template #default="{ row }">{{ getDictLabel(pointTypeDict, row.pointType) }}</template>
         </el-table-column>
-        <el-table-column prop="networkId" label="所属管网" width="130" />
+        <el-table-column label="所属管网" width="150">
+          <template #default="{ row }">{{ getNetworkName(row.networkId) }}</template>
+        </el-table-column>
         <el-table-column prop="location" label="位置" min-width="180" />
         <el-table-column prop="status" label="状态" width="90">
           <template #default="{ row }">
@@ -166,6 +168,11 @@ function getDictLabel(dict, value) {
   const item = dict.find(d => d.dictValue === String(value))
   return item ? item.dictLabel : value
 }
+function getNetworkName(networkId) {
+  if (!networkId) return '-'
+  const n = networkOptions.value.find(o => o.networkId === networkId)
+  return n ? n.networkName : networkId
+}
 
 async function loadDicts() {
   const [typeRes, statusRes] = await Promise.all([
@@ -259,7 +266,7 @@ function handleUnbind(dev) {
   }).catch(() => {})
 }
 
-onMounted(async () => { await loadDicts(); loadData() })
+onMounted(async () => { await loadDicts(); loadNetworkOptions(); loadData() })
 </script>
 
 <style scoped>

+ 19 - 17
src/views/subSystem/basic/RepairRecord.vue

@@ -122,7 +122,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'v
 import * as echarts from 'echarts'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { convertCoord } from '@/utils/coordTransform'
-import { getPipeNetworkOptions, getRepairRecordPage, addRepairRecord, updateRepairRecord, deleteRepairRecord } from '@/api/pipeNetwork/basic'
+import { getPipeNetworkOptions, getRepairRecordPage, getRepairStats, addRepairRecord, updateRepairRecord, deleteRepairRecord } from '@/api/pipeNetwork/basic'
 
 // ==================== 数据 ====================
 const loading = ref(false)
@@ -361,7 +361,7 @@ function renderMapScene() {
 let riskPieChart = null
 let repairTrendChart = null
 
-function initRiskPie() {
+function initRiskPie(data) {
   const dom = document.getElementById('riskPieChart')
   if (!dom) return
   if (riskPieChart) riskPieChart.dispose()
@@ -373,24 +373,18 @@ function initRiskPie() {
       type: 'pie', radius: ['50%', '76%'], center: ['38%', '50%'],
       avoidLabelOverlap: false, itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 3 },
       label: { show: false }, emphasis: { scale: true, scaleSize: 6 },
-      data: [
-        { value: 5, name: '抢修', itemStyle: { color: '#e63946' } },
-        { value: 5, name: '计划维修', itemStyle: { color: '#457b9d' } },
-        { value: 4, name: '防腐层修复', itemStyle: { color: '#f77f00' } },
-        { value: 4, name: '管件更换', itemStyle: { color: '#2a9d8f' } },
-        { value: 2, name: '焊缝修补', itemStyle: { color: '#6c757d' } },
-        { value: 2, name: '阀门维修', itemStyle: { color: '#b08968' } }
-      ]
+      data: (data || []).map(d => ({ value: d.value, name: d.name, itemStyle: { color: d.color } }))
     }]
   })
 }
 
-function initRepairTrend() {
+function initRepairTrend(data) {
   const dom = document.getElementById('repairTrendChart')
   if (!dom) return
   if (repairTrendChart) repairTrendChart.dispose()
   repairTrendChart = echarts.init(dom)
-  const months = ['1月', '2月', '3月', '4月', '5月', '6月']
+  const t = data || {}
+  const months = t.months || ['1月','2月','3月','4月','5月','6月']
   repairTrendChart.setOption({
     tooltip: { trigger: 'axis' },
     legend: { top: 0, textStyle: { color: '#606a78', fontSize: 11 } },
@@ -398,14 +392,22 @@ function initRepairTrend() {
     xAxis: { type: 'category', data: months, boundaryGap: false, axisLabel: { color: '#9098a6', fontSize: 11 }, axisLine: { lineStyle: { color: '#d0d5dd' } } },
     yAxis: { type: 'value', name: '次数', minInterval: 1, axisLabel: { color: '#9098a6', fontSize: 11 }, splitLine: { lineStyle: { color: '#f0f2f6', type: 'dashed' } } },
     series: [
-      { name: '高压', type: 'line', data: [2, 0, 0, 1, 1, 2], smooth: true, symbol: 'circle', symbolSize: 7, lineStyle: { width: 3, color: '#e63946' }, itemStyle: { color: '#e63946', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(230,57,70,0.12)' }, { offset: 1, color: 'rgba(230,57,70,0)' }]) } },
-      { name: '中压', type: 'line', data: [2, 1, 1, 3, 2, 1], smooth: true, symbol: 'circle', symbolSize: 7, lineStyle: { width: 3, color: '#f77f00' }, itemStyle: { color: '#f77f00', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(247,127,0,0.12)' }, { offset: 1, color: 'rgba(247,127,0,0)' }]) } },
-      { name: '低压', type: 'line', data: [1, 2, 1, 2, 1, 2], smooth: true, symbol: 'circle', symbolSize: 7, lineStyle: { width: 3, color: '#2a9d8f' }, itemStyle: { color: '#2a9d8f', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(42,157,143,0.12)' }, { offset: 1, color: 'rgba(42,157,143,0)' }]) } }
+      { name: '高压', type: 'line', data: t.high || [], smooth: true, symbol: 'circle', symbolSize: 7, lineStyle: { width: 3, color: '#e63946' }, itemStyle: { color: '#e63946', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(230,57,70,0.12)' }, { offset: 1, color: 'rgba(230,57,70,0)' }]) } },
+      { name: '中压', type: 'line', data: t.medium || [], smooth: true, symbol: 'circle', symbolSize: 7, lineStyle: { width: 3, color: '#f77f00' }, itemStyle: { color: '#f77f00', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(247,127,0,0.12)' }, { offset: 1, color: 'rgba(247,127,0,0)' }]) } },
+      { name: '低压', type: 'line', data: t.low || [], smooth: true, symbol: 'circle', symbolSize: 7, lineStyle: { width: 3, color: '#2a9d8f' }, itemStyle: { color: '#2a9d8f', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(42,157,143,0.12)' }, { offset: 1, color: 'rgba(42,157,143,0)' }]) } }
     ]
   })
 }
 
-function initCharts() { try { initRiskPie() } catch (e) { console.warn('饼图失败:', e) }; try { initRepairTrend() } catch (e) { console.warn('趋势图失败:', e) } }
+async function loadStats() {
+  try {
+    const res = await getRepairStats()
+    const d = res.data || {}
+    initRiskPie(d.typeStats)
+    initRepairTrend(d.trend)
+  } catch (e) { console.error(e) }
+}
+
 function resizeCharts() { riskPieChart?.resize(); repairTrendChart?.resize() }
 
 // ==================== 操作 ====================
@@ -441,7 +443,7 @@ onMounted(async () => {
   await loadNetworkOptions()
   await loadData()
   waitForBMapGL()
-  initCharts()
+  loadStats()
   window.addEventListener('resize', () => {
     clearTimeout(resizeTimer)
     resizeTimer = setTimeout(() => {

+ 78 - 156
src/views/subSystem/gas/gasjcyj/gxylyctx.vue

@@ -7,7 +7,7 @@
         <el-icon><Warning /></el-icon>
         <span class="title">管线压力异常报警中心</span>
         <div class="stats-badge">
-          <span class="stat-item critical"><i class="dot"></i>实时异常 {{ realtimeAlerts.length }}</span>
+          <span class="stat-item critical"><i class="dot"></i>实时异常 {{ alarmStats.realtimeCount }}</span>
           <span class="stat-item warning"><i class="dot"></i>历史异常 {{ historyAlerts.length }}</span>
           <span class="stat-item"><i class="dot"></i>监测点 {{ totalPoints }}</span>
         </div>
@@ -30,7 +30,7 @@
     <!-- KPI指标卡片 -->
     <div class="kpi-cards">
       <div class="kpi-card critical">
-        <div class="kpi-value">{{ realtimeAlerts.length }}</div>
+        <div class="kpi-value">{{ alarmStats.realtimeCount }}</div>
         <div class="kpi-label">实时异常</div>
         <div class="kpi-trend">较昨日 ↑{{ realtimeTrend }}</div>
       </div>
@@ -56,7 +56,7 @@
       <div class="alert-panel">
         <div class="panel-header">
           <span><el-icon><BellFilled /></el-icon> 实时异常提醒</span>
-          <el-tag type="danger" effect="dark">{{ realtimeAlerts.length }} 个异常</el-tag>
+          <el-tag type="danger" effect="dark">{{ alarmStats.realtimeCount }} 个异常</el-tag>
         </div>
         <div class="alert-list realtime">
           <div
@@ -84,12 +84,12 @@
               <el-button size="small" type="primary" @click.stop="handleAlert(alert)">处理</el-button>
             </div>
           </div>
-          <div v-if="realtimeAlerts.length === 0" class="empty-state">
+          <div v-if="alarmStats.realtimeCount === 0" class="empty-state">
             <el-empty description="暂无实时异常" :image-size="80" />
           </div>
         </div>
-        <div class="pagination-area" v-if="realtimeAlerts.length > pageSize">
-          <el-pagination small layout="prev, pager, next" :total="realtimeAlerts.length" :page-size="pageSize" v-model:current-page="realtimePage" />
+        <div class="pagination-area" v-if="alarmStats.realtimeCount > pageSize">
+          <el-pagination small layout="prev, pager, next" :total="alarmStats.realtimeCount" :page-size="pageSize" v-model:current-page="realtimePage" />
         </div>
       </div>
 
@@ -183,185 +183,107 @@
 </template>
 
 <script setup>
-import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
+import { ref, computed, watch, nextTick, onBeforeUnmount, onMounted } from 'vue'
 import { Warning, Refresh, Download, BellFilled, Document, Location, Search } from '@element-plus/icons-vue'
 import * as echarts from 'echarts'
 import { ElMessage } from 'element-plus'
+import { getPressureAlarmEquipment, getPressureAlarmHistory, getPressureAlarmStats, resolvePressureAlarm, dispatchPressureAlarm } from '@/api/pipeNetwork/basic'
 
-// 沅陵县压力实时异常数据
-const realtimeAlerts = ref([
-  { id: 1, pointCode: 'PR-YL-002', pointName: '辰州中路高压测点2#', address: '沅陵县太常片区辰州中路与天宁路交叉口南侧', pipelineName: '辰州中路高压段',
-    pressure: 432, flow: 2180, threshold: 420, alertTime: '2026-06-13 15:31:45', status: 'pending',
-    trendData: [390, 395, 398, 402, 405, 408, 410, 413, 416, 418, 420, 422, 424, 426, 428, 430, 431, 432, 432, 431, 430, 429, 428, 432] },
-  { id: 2, pointCode: 'PR-YL-005', pointName: '古城中路中压测点2#', address: '沅陵县太常片区古城中路太常安置区段', pipelineName: '古城中路中压段',
-    pressure: 142, flow: 860, threshold: 150, alertTime: '2026-06-13 15:29:58', status: 'pending',
-    trendData: [160, 158, 156, 155, 153, 152, 151, 150, 149, 148, 147, 146, 145, 144, 143, 142, 142, 143, 144, 145, 146, 147, 148, 142] },
-  { id: 3, pointCode: 'PR-YL-015', pointName: '建设东路高压测点4#', address: '沅陵县太常片区建设东路工业园入口处', pipelineName: '建设东路高压段',
-    pressure: 410, flow: 1950, threshold: 420, alertTime: '2026-06-13 15:31:25', status: 'pending',
-    trendData: [395, 398, 400, 402, 405, 408, 410, 412, 414, 415, 416, 417, 418, 419, 410, 409, 408, 407, 406, 405, 404, 403, 402, 410] }
-])
+// 从API加载
+const equipments = ref([])
+const selectedEquipment = ref(null)
+const historyAlerts = ref([])
+const historyPage = ref(1)
+const historyPageSize = 8
+const historyTotal = ref(0)
+const alarmStats = ref({ realtimeCount: 0, pendingCount: 0, maxPressure: 0, totalPoints: 0 })
 
-// 沅陵县压力历史异常数据
-const historyAlerts = ref([
-  { id: 101, pointCode: 'PR-YL-001', pointName: '辰州北路高压测点1#', address: '沅陵县太常片区辰州北路与建设路交叉口北侧', pipelineName: '辰州北路高压段',
-    pressure: 438, flow: 2320, threshold: 420, alertTime: '2026-06-13 09:15:00', status: 'resolved', resolveTime: '2026-06-13 10:30:00', handler: '张建国', remark: '早高峰用气波动,调压后恢复正常',
-    trendData: [380, 385, 392, 400, 410, 420, 430, 438, 438, 435, 428, 418, 405, 390, 378, 370, 365, 360, 358, 355, 352, 350, 348, 346] },
-  { id: 102, pointCode: 'PR-YL-012', pointName: '滨江东路低压测点2#', address: '沅陵县太常片区滨江东路与天宁东路交叉口', pipelineName: '滨江东路低压段',
-    pressure: 78, flow: 520, threshold: 80, alertTime: '2026-06-13 08:45:00', status: 'pending', resolveTime: null, handler: null, remark: null,
-    trendData: [95, 93, 90, 88, 85, 83, 81, 80, 78, 78, 77, 77, 76, 76, 75, 75, 74, 74, 73, 73, 72, 72, 71, 71] },
-  { id: 103, pointCode: 'PR-YL-003', pointName: '辰州南路高压测点3#', address: '沅陵县太常片区辰州南路近滨江路口', pipelineName: '辰州南路高压段',
-    pressure: 445, flow: 2480, threshold: 420, alertTime: '2026-06-12 16:20:00', status: 'resolved', resolveTime: '2026-06-12 17:45:00', handler: '李志强', remark: '管网压力波动,已稳定',
-    trendData: [390, 395, 400, 408, 418, 428, 440, 445, 445, 440, 430, 418, 405, 392, 382, 375, 370, 366, 362, 358, 355, 352, 350, 348] },
-  { id: 104, pointCode: 'PR-YL-009', pointName: '建设路中压测点1#', address: '沅陵县太常片区建设路县政府南侧', pipelineName: '建设路中压段',
-    pressure: 0, flow: 0, threshold: 150, alertTime: '2026-06-12 12:05:00', status: 'resolved', resolveTime: '2026-06-12 14:30:00', handler: '王大明', remark: '设备离线,现场重启后恢复',
-    trendData: [175, 172, 168, 165, 160, 155, 148, 135, 110, 80, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 160, 170, 175] },
-  { id: 105, pointCode: 'PR-YL-008', pointName: '迎宾东路中压测点2#', address: '沅陵县太常片区迎宾东路城郊公交站东侧', pipelineName: '迎宾东路中压段',
-    pressure: 268, flow: 1350, threshold: 250, alertTime: '2026-06-11 22:30:00', status: 'resolved', resolveTime: '2026-06-12 01:00:00', handler: '赵志远', remark: '夜间用气高峰导致瞬时超压',
-    trendData: [230, 235, 240, 245, 250, 255, 260, 265, 268, 268, 265, 258, 248, 238, 230, 225, 222, 220, 218, 215, 212, 210, 208, 206] },
-  { id: 106, pointCode: 'PR-YL-013', pointName: '天宁西路低压测点1#', address: '沅陵县太常片区天宁西路太常卫生院外墙', pipelineName: '天宁西路低压段',
-    pressure: 72, flow: 480, threshold: 80, alertTime: '2026-06-11 14:10:00', status: 'pending', resolveTime: null, handler: null, remark: null,
-    trendData: [88, 86, 84, 82, 80, 78, 76, 74, 73, 72, 72, 71, 71, 70, 70, 69, 69, 68, 68, 67, 67, 66, 66, 65] },
-  { id: 107, pointCode: 'PR-YL-006', pointName: '古城南路中压测点3#', address: '沅陵县太常片区古城南路近建设路口', pipelineName: '古城南路中压段',
-    pressure: 268, flow: 1420, threshold: 250, alertTime: '2026-06-10 10:45:00', status: 'resolved', resolveTime: '2026-06-10 12:00:00', handler: '陈文斌', remark: '午间用气小高峰,已回落正常',
-    trendData: [225, 230, 238, 245, 252, 258, 265, 268, 268, 265, 258, 248, 238, 230, 225, 220, 216, 212, 210, 208, 206, 204, 202, 200] }
-])
+async function loadEquipment() {
+  try {
+    const res = await getPressureAlarmEquipment()
+    equipments.value = (res.data || []).map(e => ({ ...e,
+      pointName: e.equipmentName, address: e.location,
+      pressure: e.latestPressure, threshold: e.latestThreshold || 250,
+      alertTime: e.latestAlarmTime,
+      level: e.hasAlarm ? (e.latestAlarmLevel === 1 ? 'critical' : 'warning') : 'normal',
+      status: e.hasAlarm ? 'pending' : 'normal'
+    }))
+  } catch (e) { console.error(e) }
+}
+const currentDeviceCode = ref('')
+async function loadHistory() {
+  try {
+    const d = dateRange.value
+    const res = await getPressureAlarmHistory(historyPage.value, historyPageSize, d?.[0] || '', d?.[1] || '', currentDeviceCode.value || '')
+    historyAlerts.value = (res.data?.records || []).map(a => ({
+      id: a.id, pointCode: a.deviceCode, pointName: a.pointName, address: a.address,
+      pressure: a.pressure, threshold: a.threshold || 250, alertTime: a.alarmTime, status: a.alarmStatus === 0 ? 'pending' : 'resolved',
+      resolveTime: a.resolveTime, handler: a.handler, remark: a.remark, flow: 0
+    }))
+    historyTotal.value = res.data?.total || 0
+  } catch (e) { console.error(e) }
+}
+function selectEquipment(eq) {
+  selectedEquipment.value = eq
+  currentDeviceCode.value = eq.equipmentCode
+  historyPage.value = 1; loadHistory()
+}
 
-// 统计数据
-const totalPoints = 15
 const realtimeTrend = 1
-const todayHistoryCount = computed(() => historyAlerts.value.filter(a => a.alertTime.startsWith('2026-06-13')).length)
-const pendingCount = computed(() => historyAlerts.value.filter(a => a.status === 'pending').length)
-const abnormalRate = computed(() => {
-  const totalAbnormal = realtimeAlerts.value.length + historyAlerts.value.filter(a => a.status === 'pending').length
-  return ((totalAbnormal / totalPoints) * 100).toFixed(1)
-})
-const avgResponseTime = computed(() => {
-  const resolved = historyAlerts.value.filter(a => a.status === 'resolved' && a.resolveTime)
-  if (!resolved.length) return '--'
-  const times = resolved.map(a => {
-    const start = new Date(a.alertTime)
-    const end = new Date(a.resolveTime)
-    return (end - start) / 60000
-  })
-  return Math.round(times.reduce((a, b) => a + b, 0) / times.length)
-})
-
-// 筛选
+const todayHistoryCount = ref(0)
+const pendingCount = computed(() => alarmStats.value.pendingCount)
+const totalPoints = computed(() => alarmStats.value.totalPoints)
+const abnormalRate = computed(() => totalPoints.value > 0 ? ((alarmStats.value.realtimeCount / totalPoints.value) * 100).toFixed(1) : 0)
+const avgResponseTime = computed(() => '--')
 const dateRange = ref([])
 const historySearch = ref('')
-const realtimePage = ref(1)
-const historyPage = ref(1)
-const pageSize = 5
-const historyPageSize = 8
-
-const filteredHistory = computed(() => {
-  let list = historyAlerts.value
-  if (dateRange.value && dateRange.value.length === 2) {
-    const start = new Date(dateRange.value[0])
-    const end = new Date(dateRange.value[1])
-    list = list.filter(a => {
-      const alertDate = new Date(a.alertTime)
-      return alertDate >= start && alertDate <= end
-    })
-  }
-  if (historySearch.value) {
-    const kw = historySearch.value.toLowerCase()
-    list = list.filter(a => a.pointName.toLowerCase().includes(kw) || a.address.toLowerCase().includes(kw))
-  }
-  return list
-})
-
-const paginatedRealtime = computed(() => {
-  const start = (realtimePage.value - 1) * pageSize
-  return realtimeAlerts.value.slice(start, start + pageSize)
-})
+const paginatedHistory = computed(() => historyAlerts.value)
 
-const paginatedHistory = computed(() => {
-  const start = (historyPage.value - 1) * historyPageSize
-  return filteredHistory.value.slice(start, start + historyPageSize)
-})
-
-// 当前选中告警
 const currentAlert = ref(null)
 const detailDrawerVisible = ref(false)
 const chartRef = ref(null)
 let chartInstance = null
 
 function renderChart() {
-  if (!chartRef.value || !currentAlert.value?.trendData) return
-  if (!chartInstance) {
-    chartInstance = echarts.init(chartRef.value)
-  }
-  const times = Array.from({ length: 24 }, (_, i) => `${i}:00`)
+  if (!chartRef.value || !currentAlert.value) return
+  if (!chartInstance) chartInstance = echarts.init(chartRef.value)
   const threshold = currentAlert.value.threshold
   chartInstance.setOption({
     tooltip: { trigger: 'axis' },
-    xAxis: { type: 'category', data: times, axisLabel: { rotate: 45, interval: 4 } },
-    yAxis: { type: 'value', name: '压力 (kPa)' },
+    xAxis: { type: 'category', data: Array.from({length:24},(_,i)=>`${i}:00`), axisLabel:{rotate:45,interval:4} },
+    yAxis: { type: 'value', name: '压力(kPa)' },
     series: [
-      { name: '实时压力', type: 'line', data: currentAlert.value.trendData, smooth: true, lineStyle: { color: '#f56c6c', width: 2 }, areaStyle: { opacity: 0.1 } },
-      { name: '报警阈值', type: 'line', data: Array(24).fill(threshold), lineStyle: { color: '#e6a23c', width: 2, type: 'dashed' }, symbol: 'none' }
+      { name:'实时压力', type:'line', data: Array(24).fill(currentAlert.value.pressure||0), smooth:true, lineStyle:{color:'#f56c6c',width:2}, areaStyle:{opacity:0.1} },
+      { name:'报警阈值', type:'line', data: Array(24).fill(threshold), lineStyle:{color:'#e6a23c',width:2,type:'dashed'}, symbol:'none' }
     ]
   })
 }
-
-function onDrawerOpened() {
-  nextTick(() => renderChart())
-}
-
-// 方法
-const viewAlertDetail = (alert) => {
-  currentAlert.value = alert
-  detailDrawerVisible.value = true
-}
-
-const handleAlert = (alert) => {
-  currentAlert.value = alert
-  detailDrawerVisible.value = true
-}
-
-const resolveAlert = () => {
-  if (currentAlert.value.status === 'pending') {
-    // 从实时异常移到历史异常
-    const index = realtimeAlerts.value.findIndex(a => a.id === currentAlert.value.id)
-    if (index !== -1) {
-      const resolved = { ...currentAlert.value, status: 'resolved', resolveTime: new Date().toLocaleString(), handler: '当前用户' }
-      realtimeAlerts.value.splice(index, 1)
-      historyAlerts.value.unshift(resolved)
-    } else {
-      // 如果是历史异常中的待处理
-      const histIndex = historyAlerts.value.findIndex(a => a.id === currentAlert.value.id)
-      if (histIndex !== -1) {
-        historyAlerts.value[histIndex].status = 'resolved'
-        historyAlerts.value[histIndex].resolveTime = new Date().toLocaleString()
-        historyAlerts.value[histIndex].handler = '当前用户'
-      }
-    }
-    currentAlert.value.status = 'resolved'
-    ElMessage.success('已标记为已处理')
-    detailDrawerVisible.value = false
+function onDrawerOpened() { nextTick(() => renderChart()) }
+const viewAlertDetail = (a) => { currentAlert.value = a; detailDrawerVisible.value = true }
+const handleAlert = (a) => { currentAlert.value = a; detailDrawerVisible.value = true }
+const resolveAlert = async () => {
+  if (currentAlert.value?.status === 'pending') {
+    try {
+      await resolvePressureAlarm(currentAlert.value.id)
+      currentAlert.value.status = 'resolved'
+      ElMessage.success('已处理'); detailDrawerVisible.value = false
+      loadEquipment(); loadHistory(); loadStats()
+    } catch (e) { ElMessage.error('处理失败') }
   }
 }
-
-const dispatchWorkOrder = () => {
-  ElMessage.success('已派发维修工单')
-}
-
-const refreshData = () => {
-  ElMessage.success('数据已刷新')
+const dispatchWorkOrder = async () => {
+  if (currentAlert.value?.id) { try { await dispatchPressureAlarm(currentAlert.value.id); ElMessage.success('已派发工单(伪)') } catch (e) { ElMessage.error('派发失败') } }
 }
-
-const exportReport = () => {
-  ElMessage.success('导出报表(演示)')
+async function loadStats() {
+  try { const res = await getPressureAlarmStats(); if (res.data) alarmStats.value = res.data } catch (e) { console.error(e) }
 }
+const refreshData = () => { loadEquipment(); loadStats() }
+const exportReport = () => { ElMessage.success('导出报表(演示)') }
 
-// 监听日期范围变化重置页码
-watch([dateRange, historySearch], () => { historyPage.value = 1 })
+watch(dateRange, () => { historyPage.value = 1; loadHistory() })
 
-onBeforeUnmount(() => {
-  try { chartInstance?.dispose() } catch {}
-  chartInstance = null
-})
+onMounted(async () => { await loadEquipment(); await loadHistory(); await loadStats() })
+onBeforeUnmount(() => { try { chartInstance?.dispose() } catch {}; chartInstance = null })
 </script>
 
 <style scoped>

+ 121 - 184
src/views/subSystem/gas/gasjcyj/krqindbj.vue

@@ -7,9 +7,9 @@
         <el-icon><Warning /></el-icon>
         <span class="title">可燃气体中心</span>
         <div class="stats-badge">
-          <span class="stat-item critical"><i class="dot"></i>实时报警 {{ realtimeAlerts.length }}</span>
-          <span class="stat-item warning"><i class="dot"></i>历史报警 {{ historyAlerts.length }}</span>
-          <span class="stat-item"><i class="dot"></i>接入场站 {{ totalStations }}</span>
+          <span class="stat-item critical"><i class="dot"></i>报警设备 {{ alarmStats.realtimeCount }}</span>
+          <span class="stat-item warning"><i class="dot"></i>设备总数 {{ equipments.length }}</span>
+          <span class="stat-item"><i class="dot"></i>最高浓度 {{ alarmStats.maxConcentration }}ppm</span>
         </div>
       </div>
       <div class="header-right">
@@ -33,7 +33,7 @@
     <!-- KPI指标卡片 -->
     <div class="kpi-cards">
       <div class="kpi-card critical">
-        <div class="kpi-value">{{ realtimeAlerts.length }}</div>
+        <div class="kpi-value">{{ alarmStats.realtimeCount }}</div>
         <div class="kpi-label">实时报警</div>
         <div class="kpi-trend">较昨日 ↑{{ realtimeTrend }}</div>
       </div>
@@ -55,50 +55,45 @@
 
     <!-- 主内容区:左侧实时报警 + 右侧历史报警 -->
     <div class="main-layout">
-      <!-- 左侧实时报警列表 -->
+      <!-- 左侧设备列表 -->
       <div class="alert-panel">
         <div class="panel-header">
-          <span><el-icon><BellFilled /></el-icon> 实时报警</span>
-          <el-tag type="danger" effect="dark">{{ realtimeAlerts.length }} 个报警</el-tag>
+          <span><el-icon><BellFilled /></el-icon> 探测器设备</span>
+          <el-tag type="danger" effect="dark">{{ alarmStats.realtimeCount }} 个报警</el-tag>
         </div>
         <div class="alert-list realtime">
           <div
-              v-for="alert in paginatedRealtime"
-              :key="alert.id"
+              v-for="eq in equipments"
+              :key="eq.equipmentId"
               class="alert-item"
-              :class="{ critical: alert.level === 'critical', warning: alert.level === 'warning' }"
-              @click="viewAlertDetail(alert)"
+              :class="{ critical: eq.level === 'critical', warning: eq.level === 'warning', selected: selectedEquipment?.equipmentId === eq.equipmentId }"
+              @click="selectEquipment(eq)"
           >
             <div class="alert-level">
-              <el-tag :type="alert.level === 'critical' ? 'danger' : 'warning'" size="small">
-                {{ alert.level === 'critical' ? '严重' : '预警' }}
+              <el-tag :type="eq.level === 'critical' ? 'danger' : eq.level === 'warning' ? 'warning' : 'info'" size="small">
+                {{ eq.level === 'critical' ? '严重' : eq.level === 'warning' ? '预警' : '正常' }}
               </el-tag>
             </div>
             <div class="alert-content">
-              <div class="alert-title">{{ alert.stationName }}</div>
+              <div class="alert-title">{{ eq.equipmentName }}</div>
               <div class="alert-company">
-                <el-icon><OfficeBuilding /></el-icon> {{ alert.companyName }}
+                <el-icon><OfficeBuilding /></el-icon> 沅陵燃气
               </div>
               <div class="alert-location">
-                <el-icon><Location /></el-icon> {{ alert.address }}
+                <el-icon><Location /></el-icon> {{ eq.location }}
               </div>
               <div class="alert-data">
-                <span class="data-item">甲烷浓度: <strong :class="{ 'critical-value': alert.concentration > 30 }">{{ alert.concentration }} ppm</strong></span>
-                <span class="data-item">报警阈值: {{ alert.threshold }} ppm</span>
+                <span class="data-item" v-if="eq.latestConcentration != null">甲烷浓度: <strong :class="{ 'critical-value': eq.latestConcentration > 30 }">{{ eq.latestConcentration }} ppm</strong></span>
+                <span class="data-item" v-if="eq.latestThreshold != null">阈值: {{ eq.latestThreshold }} ppm</span>
+                <span class="data-item" v-if="eq.latestConcentration == null" style="color:#67c23a">无报警记录</span>
               </div>
-              <div class="alert-time">{{ alert.alertTime }}</div>
-            </div>
-            <div class="alert-action">
-              <el-button size="small" type="primary" @click.stop="handleAlert(alert)">处理</el-button>
+              <div class="alert-time" v-if="eq.alertTime">{{ eq.alertTime }}</div>
             </div>
           </div>
-          <div v-if="realtimeAlerts.length === 0" class="empty-state">
-            <el-empty description="暂无实时报警" :image-size="80" />
+          <div v-if="equipments.length === 0" class="empty-state">
+            <el-empty description="暂无探测器设备" :image-size="80" />
           </div>
         </div>
-        <div class="pagination-area" v-if="realtimeAlerts.length > pageSize">
-          <el-pagination small layout="prev, pager, next" :total="realtimeAlerts.length" :page-size="pageSize" v-model:current-page="realtimePage" />
-        </div>
       </div>
 
       <!-- 右侧历史报警列表 -->
@@ -151,9 +146,10 @@
           <el-pagination
               small
               layout="total, prev, pager, next"
-              :total="filteredHistory.length"
+              :total="historyTotal"
               :page-size="historyPageSize"
               v-model:current-page="historyPage"
+              @current-change="loadHistory"
           />
         </div>
       </div>
@@ -204,149 +200,80 @@
 </template>
 
 <script setup>
-import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
+import { ref, computed, watch, nextTick, onBeforeUnmount, onMounted } from 'vue'
 import { Warning, Refresh, Download, BellFilled, Document, Location, Search, OfficeBuilding } from '@element-plus/icons-vue'
 import * as echarts from 'echarts'
 import { ElMessage } from 'element-plus'
+import { getGasAlarmEquipment, getGasAlarmHistory, getGasAlarmStats, resolveGasAlarm, dispatchGasAlarm } from '@/api/pipeNetwork/basic'
 
-// 沅陵县场站可燃气体实时报警数据
-const realtimeAlerts = ref([
-  { id: 1, stationCode: 'YL-LNG-01', stationName: '太常LNG储配站', companyName: '沅陵燃气', companyId: 'yuanling',
-    address: '沅陵县太常片区辰州东路末段LNG储配站', concentration: 48, threshold: 20, level: 'critical',
-    alertTime: '2026-06-13 15:23:45', status: 'pending',
-    trendData: [12, 15, 18, 22, 28, 32, 38, 42, 45, 48, 48, 47, 45, 42, 38, 35, 32, 28, 25, 22, 20, 18, 16, 14] },
-  { id: 2, stationCode: 'YL-TY-01', stationName: '太常调压站', companyName: '沅陵燃气', companyId: 'yuanling',
-    address: '沅陵县太常片区古城中路与天宁路交叉口东侧', concentration: 28, threshold: 20, level: 'warning',
-    alertTime: '2026-06-13 14:58:12', status: 'pending',
-    trendData: [15, 17, 19, 22, 24, 26, 28, 28, 27, 26, 25, 24, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11] },
-  { id: 3, stationCode: 'YL-GY-01', stationName: '工业园计量站', companyName: '沅陵燃气', companyId: 'yuanling',
-    address: '沅陵县太常片区建设东路工业园入口东侧', concentration: 35, threshold: 20, level: 'critical',
-    alertTime: '2026-06-13 14:32:05', status: 'pending',
-    trendData: [18, 20, 22, 25, 28, 30, 32, 35, 35, 34, 33, 32, 30, 28, 26, 24, 22, 20, 19, 18, 17, 16, 15, 14] }
-])
+// 左侧: 所有探测器设备(去重)
+const equipments = ref([])
+const selectedEquipment = ref(null)
+// 右侧: 历史报警
+const historyAlerts = ref([])
+const historyPage = ref(1)
+const historyPageSize = 8
+const historyTotal = ref(0)
+const alarmStats = ref({ realtimeCount: 0, pendingCount: 0, maxConcentration: 0, totalStations: 0 })
 
-// 沅陵县场站可燃气体历史报警数据
-const historyAlerts = ref([
-  { id: 101, stationCode: 'YL-MZ-01', stationName: '太常天然气门站', companyName: '沅陵燃气', companyId: 'yuanling',
-    address: '沅陵县太常片区辰州北路末段天然气门站', concentration: 42, threshold: 20, level: 'critical',
-    alertTime: '2026-06-13 08:30:00', status: 'resolved', resolveTime: '2026-06-13 09:45:00', handler: '张建国', remark: '法兰连接处微漏,紧固后恢复正常',
-    trendData: [10, 12, 14, 18, 22, 28, 32, 36, 40, 42, 42, 41, 38, 34, 28, 22, 16, 12, 10, 9, 8, 8, 7, 7] },
-  { id: 102, stationCode: 'YL-CBTY-01', stationName: '城北调压站', companyName: '沅陵燃气', companyId: 'yuanling',
-    address: '沅陵县太常片区天宁西路与古城北路交叉口', concentration: 25, threshold: 20, level: 'warning',
-    alertTime: '2026-06-13 07:15:00', status: 'resolved', resolveTime: '2026-06-13 08:20:00', handler: '李志强', remark: '通风后浓度下降至正常范围',
-    trendData: [12, 13, 15, 17, 19, 21, 23, 25, 25, 24, 23, 21, 18, 15, 13, 11, 10, 9, 8, 8, 7, 7, 6, 6] },
-  { id: 103, stationCode: 'YL-CNTY-01', stationName: '城南调压站', companyName: '沅陵燃气', companyId: 'yuanling',
-    address: '沅陵县太常片区建设路与古城南路交叉口南侧', concentration: 52, threshold: 20, level: 'critical',
-    alertTime: '2026-06-12 22:45:00', status: 'pending', resolveTime: null, handler: null, remark: null,
-    trendData: [18, 20, 22, 26, 30, 34, 38, 42, 46, 50, 52, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40] },
-  { id: 104, stationCode: 'YL-TY-01', stationName: '太常调压站', companyName: '沅陵燃气', companyId: 'yuanling',
-    address: '沅陵县太常片区古城中路与天宁路交叉口东侧', concentration: 22, threshold: 20, level: 'warning',
-    alertTime: '2026-06-12 14:20:00', status: 'resolved', resolveTime: '2026-06-12 15:10:00', handler: '王大明', remark: '巡检期间短暂波动,已自动恢复',
-    trendData: [10, 11, 13, 15, 18, 20, 22, 22, 21, 20, 18, 16, 14, 12, 10, 9, 8, 8, 7, 7, 6, 6, 5, 5] },
-  { id: 105, stationCode: 'YL-LNG-01', stationName: '太常LNG储配站', companyName: '沅陵燃气', companyId: 'yuanling',
-    address: '沅陵县太常片区辰州东路末段LNG储配站', concentration: 38, threshold: 20, level: 'critical',
-    alertTime: '2026-06-11 10:00:00', status: 'resolved', resolveTime: '2026-06-11 11:30:00', handler: '赵志远', remark: '储罐区检测器校准后读数正常',
-    trendData: [8, 10, 14, 18, 24, 30, 35, 38, 38, 37, 35, 30, 24, 18, 14, 10, 8, 7, 6, 6, 5, 5, 4, 4] },
-  { id: 106, stationCode: 'YL-GY-01', stationName: '工业园计量站', companyName: '沅陵燃气', companyId: 'yuanling',
-    address: '沅陵县太常片区建设东路工业园入口东侧', concentration: 26, threshold: 20, level: 'warning',
-    alertTime: '2026-06-11 16:10:00', status: 'pending', resolveTime: null, handler: null, remark: null,
-    trendData: [14, 15, 16, 18, 20, 22, 24, 26, 26, 27, 28, 28, 27, 27, 26, 26, 25, 25, 24, 24, 23, 23, 22, 22] },
-  { id: 107, stationCode: 'YL-CBTY-01', stationName: '城北调压站', companyName: '沅陵燃气', companyId: 'yuanling',
-    address: '沅陵县太常片区天宁西路与古城北路交叉口', concentration: 24, threshold: 20, level: 'warning',
-    alertTime: '2026-06-10 09:30:00', status: 'resolved', resolveTime: '2026-06-10 10:15:00', handler: '陈文斌', remark: '调压器维护后短暂波动,已稳定',
-    trendData: [11, 13, 15, 18, 21, 23, 24, 24, 23, 21, 18, 15, 12, 10, 9, 8, 7, 7, 6, 6, 5, 5, 5, 4] },
-  { id: 108, stationCode: 'YL-CNTY-01', stationName: '城南调压站', companyName: '沅陵燃气', companyId: 'yuanling',
-    address: '沅陵县太常片区建设路与古城南路交叉口南侧', concentration: 31, threshold: 20, level: 'critical',
-    alertTime: '2026-06-12 09:50:00', status: 'resolved', resolveTime: '2026-06-12 11:15:00', handler: '李志强', remark: '阀门密封圈老化,更换后恢复正常',
-    trendData: [12, 14, 16, 19, 22, 26, 29, 31, 31, 30, 28, 25, 21, 16, 12, 9, 7, 6, 5, 5, 4, 4, 3, 3] },
-  { id: 109, stationCode: 'YL-MZ-01', stationName: '太常天然气门站', companyName: '沅陵燃气', companyId: 'yuanling',
-    address: '沅陵县太常片区辰州北路末段天然气门站', concentration: 23, threshold: 20, level: 'warning',
-    alertTime: '2026-06-11 18:30:00', status: 'resolved', resolveTime: '2026-06-11 19:10:00', handler: '张建国', remark: '交接班巡检发现,通风处置后恢复',
-    trendData: [13, 14, 16, 18, 20, 22, 23, 23, 22, 20, 17, 14, 12, 10, 9, 8, 7, 7, 6, 6, 5, 5, 5, 4] },
-  { id: 110, stationCode: 'YL-TY-01', stationName: '太常调压站', companyName: '沅陵燃气', companyId: 'yuanling',
-    address: '沅陵县太常片区古城中路与天宁路交叉口东侧', concentration: 33, threshold: 20, level: 'critical',
-    alertTime: '2026-06-10 15:40:00', status: 'pending', resolveTime: null, handler: null, remark: null,
-    trendData: [15, 17, 19, 22, 25, 28, 30, 32, 33, 33, 33, 32, 32, 31, 31, 30, 30, 29, 29, 28, 28, 27, 27, 26] },
-  { id: 111, stationCode: 'YL-LNG-01', stationName: '太常LNG储配站', companyName: '沅陵燃气', companyId: 'yuanling',
-    address: '沅陵县太常片区辰州东路末段LNG储配站', concentration: 21, threshold: 20, level: 'warning',
-    alertTime: '2026-06-10 11:20:00', status: 'resolved', resolveTime: '2026-06-10 12:05:00', handler: '赵志远', remark: '槽车卸液作业期间瞬时波动',
-    trendData: [8, 9, 11, 14, 17, 19, 21, 21, 20, 18, 15, 12, 10, 8, 7, 6, 5, 5, 4, 4, 3, 3, 3, 2] }
-])
+async function loadEquipment() {
+  try {
+    const res = await getGasAlarmEquipment()
+    equipments.value = (res.data || []).map(e => ({
+      ...e,
+      stationName: e.equipmentName,
+      concentration: e.latestConcentration,
+      threshold: e.latestThreshold || 20,
+      alertTime: e.latestAlarmTime,
+      level: e.hasAlarm ? (e.latestAlarmLevel === 1 ? 'critical' : 'warning') : 'normal',
+      status: e.hasAlarm ? 'pending' : 'normal'
+    }))
+  } catch (e) { console.error(e) }
+}
+const currentDeviceCode = ref('')
+async function loadHistory() {
+  try {
+    const d = dateRange.value
+    const res = await getGasAlarmHistory(historyPage.value, historyPageSize, d?.[0] || '', d?.[1] || '', currentDeviceCode.value || '')
+    historyAlerts.value = (res.data?.records || []).map(mapAlarm)
+    historyTotal.value = res.data?.total || 0
+  } catch (e) { console.error(e) }
+}
+function selectEquipment(eq) {
+  selectedEquipment.value = eq
+  currentDeviceCode.value = eq.equipmentCode
+  historyPage.value = 1
+  loadHistory()
+}
+function mapAlarm(a) {
+  const lv = (a.alarmLevel === 1 || a.alarmLevelText === '严重') ? 'critical' : 'warning'
+  const status = (a.alarmStatus === 0 || a.statusText === '待处理') ? 'pending' : 'resolved'
+  return {
+    id: a.id, deviceCode: a.deviceCode,
+    stationName: a.stationName || a.deviceCode,
+    companyName: '沅陵燃气', companyId: 'yuanling',
+    address: a.address || '',
+    concentration: a.concentration || a.actualValue || 0,
+    threshold: a.threshold || a.maxValue || 20,
+    level: lv, alertTime: a.alarmTime || '', status,
+    resolveTime: a.resolveTime || a.handleTime || null,
+    handler: a.handler || a.handleUser || null,
+    remark: a.remark || a.handleRemark || null
+  }
+}
 
-// 统计数据
-const totalStations = computed(() => {
-  const all = [...realtimeAlerts.value, ...historyAlerts.value]
-  return new Set(all.map(a => a.stationCode)).size
-})
 const realtimeTrend = 1
-const todayHistoryCount = computed(() => historyAlerts.value.filter(a => a.alertTime.startsWith('2026-06-13')).length)
-const pendingCount = computed(() => historyAlerts.value.filter(a => a.status === 'pending').length)
-const maxConcentration = computed(() => {
-  const all = [...realtimeAlerts.value, ...historyAlerts.value]
-  return Math.max(...all.map(a => a.concentration))
-})
-const maxConcentrationPoint = computed(() => {
-  const all = [...realtimeAlerts.value, ...historyAlerts.value]
-  const max = Math.max(...all.map(a => a.concentration))
-  return all.find(a => a.concentration === max)?.stationName || '—'
-})
-const avgResponseTime = computed(() => {
-  const resolved = historyAlerts.value.filter(a => a.status === 'resolved' && a.resolveTime)
-  if (!resolved.length) return '--'
-  const times = resolved.map(a => {
-    const start = new Date(a.alertTime)
-    const end = new Date(a.resolveTime)
-    return (end - start) / 60000
-  })
-  return Math.round(times.reduce((a, b) => a + b, 0) / times.length)
-})
+const todayHistoryCount = computed(() => equipments.value.filter(e => e.alertTime && e.alertTime.startsWith(new Date().toISOString().substring(0,10))).length)
+const pendingCount = computed(() => alarmStats.value.pendingCount)
+const maxConcentration = computed(() => alarmStats.value.maxConcentration)
+const maxConcentrationPoint = computed(() => alarmStats.value.maxConcentrationPoint || '—')
+const avgResponseTime = computed(() => '--')
 
 // 筛选
 const dateRange = ref([])
 const companyFilter = ref('')
 const historySearch = ref('')
-const realtimePage = ref(1)
-const historyPage = ref(1)
-const pageSize = 5
-const historyPageSize = 8
-
-const filteredHistory = computed(() => {
-  let list = historyAlerts.value
-  if (dateRange.value && dateRange.value.length === 2) {
-    const start = new Date(dateRange.value[0])
-    const end = new Date(dateRange.value[1])
-    list = list.filter(a => {
-      const alertDate = new Date(a.alertTime)
-      return alertDate >= start && alertDate <= end
-    })
-  }
-  if (companyFilter.value) {
-    list = list.filter(a => a.companyId === companyFilter.value)
-  }
-  if (historySearch.value) {
-    const kw = historySearch.value.toLowerCase()
-    list = list.filter(a => a.stationName.toLowerCase().includes(kw) || a.address.toLowerCase().includes(kw))
-  }
-  return list
-})
-
-const filteredRealtime = computed(() => {
-  let list = realtimeAlerts.value
-  if (companyFilter.value) {
-    list = list.filter(a => a.companyId === companyFilter.value)
-  }
-  return list
-})
-
-const paginatedRealtime = computed(() => {
-  const start = (realtimePage.value - 1) * pageSize
-  return filteredRealtime.value.slice(start, start + pageSize)
-})
-
-const paginatedHistory = computed(() => {
-  const start = (historyPage.value - 1) * historyPageSize
-  return filteredHistory.value.slice(start, start + historyPageSize)
-})
+const paginatedHistory = computed(() => historyAlerts.value)
 
 // 当前选中报警
 const currentAlert = ref(null)
@@ -387,33 +314,43 @@ const handleAlert = (alert) => {
   detailDrawerVisible.value = true
 }
 
-const resolveAlert = () => {
-  if (currentAlert.value.status === 'pending') {
-    // 从实时报警移到历史报警
-    const index = realtimeAlerts.value.findIndex(a => a.id === currentAlert.value.id)
-    if (index !== -1) {
-      const resolved = { ...currentAlert.value, status: 'resolved', resolveTime: new Date().toLocaleString(), handler: '当前用户' }
-      realtimeAlerts.value.splice(index, 1)
-      historyAlerts.value.unshift(resolved)
-    } else {
-      // 如果是历史报警中的待处理
-      const histIndex = historyAlerts.value.findIndex(a => a.id === currentAlert.value.id)
-      if (histIndex !== -1) {
-        historyAlerts.value[histIndex].status = 'resolved'
-        historyAlerts.value[histIndex].resolveTime = new Date().toLocaleString()
-        historyAlerts.value[histIndex].handler = '当前用户'
-      }
-    }
-    currentAlert.value.status = 'resolved'
-    ElMessage.success('已标记为已处理')
-    detailDrawerVisible.value = false
+const resolveAlert = async () => {
+  if (currentAlert.value?.status === 'pending') {
+    try {
+      await resolveGasAlarm(currentAlert.value.id)
+      currentAlert.value.status = 'resolved'
+      ElMessage.success('已标记为已处理')
+      detailDrawerVisible.value = false
+      loadEquipment(); loadHistory(); loadStats()
+    } catch (e) { ElMessage.error('处理失败') }
+  }
+}
+
+const dispatchWorkOrder = async () => {
+  if (currentAlert.value?.id) {
+    try {
+      await dispatchGasAlarm(currentAlert.value.id)
+      ElMessage.success('已派发现场处置工单(伪)')
+    } catch (e) { ElMessage.error('派发失败') }
   }
 }
 
-const dispatchWorkOrder = () => {
-  ElMessage.success('已派发现场处置工单')
+async function loadStats() {
+  try {
+    const res = await getGasAlarmStats()
+    if (res.data) {
+      alarmStats.value = res.data
+      totalStations.value = res.data.totalStations || 0
+    }
+  } catch (e) { console.error(e) }
 }
 
+onMounted(async () => {
+  await loadEquipment()
+  await loadHistory()
+  await loadStats()
+})
+
 const refreshData = () => {
   ElMessage.success('数据已刷新')
 }
@@ -422,9 +359,8 @@ const exportReport = () => {
   ElMessage.success('导出报表(演示)')
 }
 
-// 监听筛选变化重置页码
-watch([dateRange, companyFilter, historySearch], () => { historyPage.value = 1 })
-watch(companyFilter, () => { realtimePage.value = 1 })
+// 日期范围变化重载历史
+watch(dateRange, () => { historyPage.value = 1; loadHistory() })
 
 onBeforeUnmount(() => {
   try { chartInstance?.dispose() } catch {}
@@ -523,6 +459,7 @@ onBeforeUnmount(() => {
 }
 .alert-item.critical { background: #fff8f8; border: 1px solid #ffd4d4; }
 .alert-item.warning { background: #fffef5; border: 1px solid #ffe4b5; }
+.alert-item.selected { background: #ecf5ff; border: 2px solid #409eff; }
 .alert-item:hover { transform: translateX(2px); box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
 .alert-level { flex-shrink: 0; }
 .alert-content { flex: 1; }

+ 17 - 13
src/views/subSystem/gas/ghome/gHome.vue

@@ -23,11 +23,14 @@ const baseCards = computed(() => {
   ]
 })
 
-const operationSummary = [
-  { label: '管网总长度', value: '128.6km' },
-  { label: '重点管段', value: '12段' },
-  { label: '调压站', value: '6座' }
-]
+const operationSummary = computed(() => {
+  const ps = dashboardData.value.pipeStats || {}
+  return [
+    { label: '管网总长度', value: (ps.totalLength || 0) + 'km' },
+    { label: '燃气管网', value: (ps.gasNetworks || 0) + '条' },
+    { label: '管网总数', value: (ps.totalNetworks || 0) + '条' }
+  ]
+})
 
 const repairRecords = computed(() => {
   const list = dashboardData.value.repairTrend || []
@@ -64,12 +67,13 @@ const hazardSummary = computed(() => {
 
 const mapKpis = computed(() => {
   const bs = dashboardData.value.baseStats || {}
+  const es = dashboardData.value.equipmentStats || {}
   const total = bs.totalPoints || 0
   const online = bs.onlinePoints || 0
   return [
     { label: '在线率', value: total > 0 ? (online / total * 100).toFixed(1) + '%' : '--' },
-    { label: '压力均值', value: '0.21MPa' },
-    { label: '今日告警', value: (bs.alarmCount || 0) + '条' }
+    { label: '压力均值', value: (es.avgPressure || 0) + 'MPa' },
+    { label: '今日告警', value: (es.todayAlarm || 0) + '条' }
   ]
 })
 
@@ -417,16 +421,16 @@ onBeforeUnmount(() => {
 
           <div class="map-panel info-panel">
             <div class="mini-title">当前管网信息</div>
-            <div class="mini-row"><span>主干线</span><strong>2条</strong></div>
-            <div class="mini-row"><span>支线</span><strong>4条</strong></div>
-            <div class="mini-row"><span>重点巡检</span><strong>3段</strong></div>
+            <div class="mini-row"><span>管网总长度</span><strong>{{ (dashboardData.pipeStats && dashboardData.pipeStats.totalLength) || 0 }}km</strong></div>
+            <div class="mini-row"><span>燃气管网</span><strong>{{ (dashboardData.pipeStats && dashboardData.pipeStats.gasNetworks) || 0 }}条</strong></div>
+            <div class="mini-row"><span>设备总数</span><strong>{{ (dashboardData.equipmentStats && dashboardData.equipmentStats.totalEquipment) || 0 }}台</strong></div>
           </div>
 
           <div class="map-panel point-panel">
             <div class="mini-title">当前管点信息</div>
-            <div class="mini-row"><span>在线采集</span><strong>16个</strong></div>
-            <div class="mini-row"><span>预警点位</span><strong>3个</strong></div>
-            <div class="mini-row"><span>离线点位</span><strong>2个</strong></div>
+            <div class="mini-row"><span>监测点总数</span><strong>{{ (dashboardData.baseStats && dashboardData.baseStats.totalPoints) || 0 }}个</strong></div>
+            <div class="mini-row"><span>在线监测点</span><strong>{{ (dashboardData.baseStats && dashboardData.baseStats.onlinePoints) || 0 }}个</strong></div>
+            <div class="mini-row"><span>离线监测点</span><strong>{{ (dashboardData.baseStats && dashboardData.baseStats.offlinePoints) || 0 }}个</strong></div>
           </div>
         </div>
       </div>