瀏覽代碼

bug修改

LAPTOP-JI2IUVG1\26646 22 小時之前
父節點
當前提交
cb8e87fe00

+ 2 - 0
src/App.vue

@@ -5,8 +5,10 @@
 <script setup>
 import useSettingsStore from '@/store/modules/settings'
 import { handleThemeStyle } from '@/utils/theme'
+import { startWorkOrderReminder } from '@/utils/deviceStatusWebSocket'
 
 onMounted(() => {
+  startWorkOrderReminder()
   nextTick(() => {
     // 初始化主题样式
     handleThemeStyle(useSettingsStore().theme)

+ 9 - 4
src/api/drainage.js

@@ -239,6 +239,11 @@ export function handleAlarm(alarmId, handleUser, handleRemark) {
   })
 }
 
+// 报警审核通过后自动创建排水运维工单
+export function approveAlarmToWorkOrder(data) {
+  return request({ url: '/api/alarm/approve-to-work-order', method: 'post', data })
+}
+
 // 根据ID获取报警详情
 export function getAlarmById(id) {
   return request({
@@ -298,11 +303,11 @@ export function getThresholdList(params) {
 export function getThresholdById(id) {
   return request({ url: '/warningThreshold/getById/' + id, method: 'get' })
 }
-export function saveThreshold(data) {
-  return request({ url: '/warningThreshold/save', method: 'post', data })
+export function saveThreshold(data, moduleType) {
+  return request({ url: '/warningThreshold/save', method: 'post', data, params: moduleType ? { moduleType } : undefined })
 }
-export function updateThreshold(data) {
-  return request({ url: '/warningThreshold/update', method: 'post', data })
+export function updateThreshold(data, moduleType) {
+  return request({ url: '/warningThreshold/update', method: 'post', data, params: moduleType ? { moduleType } : undefined })
 }
 export function deleteThreshold(ids) {
   return request({ url: '/warningThreshold/deleteBatch', method: 'post', data: ids })

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

@@ -457,8 +457,13 @@ export function getGisEquipments(params) {
 export function getWarningThresholdPage(pageNum, pageSize, params) {
   return request({ url: '/warningThreshold/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
 }
-export function getWarningThresholdModulePage(pageNum, pageSize, typeName, params) {
-  return request({ url: '/warningThreshold/findByModulePage', method: 'get', params: { pageNum, pageSize, typeName, ...params } })
+export function getWarningThresholdModulePage(pageNum, pageSize, typeNameOrParams, moduleParams) {
+  // 新版阈值页面按固定业务模块查询,不再把“燃气”作为查询条件传给后端。
+  // 保留旧的四参数调用方式,兼容其他页面。
+  const params = moduleParams === undefined
+    ? (typeNameOrParams || {})
+    : { typeName: typeNameOrParams, ...(moduleParams || {}) }
+  return request({ url: '/warningThreshold/findByModulePage', method: 'get', params: { pageNum, pageSize, ...params } })
 }
 export function getWarningThresholdList(params) {
   return request({ url: '/warningThreshold/getWarningThresholdList', method: 'get', params })

+ 3 - 0
src/api/pipeNetwork/hazard.js

@@ -94,3 +94,6 @@ export function deleteRisk(id) {
 export function getFourColorMap() {
   return request({ url: '/api/risk/four-color-map', method: 'get' })
 }
+export function getGasRiskAssessments() {
+  return request({ url: '/api/risk/gas-assessments', method: 'get' })
+}

+ 106 - 0
src/utils/deviceStatusWebSocket.js

@@ -0,0 +1,106 @@
+import { ref } from 'vue'
+import { ElNotification } from 'element-plus'
+import { getToken } from '@/utils/auth'
+
+const DEFAULT_URL = 'ws://localhost:8301/ws/deviceStatus'
+
+export const workOrders = ref([])
+export const workOrderTotal = ref(0)
+export const workOrderWsState = ref('closed')
+
+let socket = null
+let heartbeatTimer = null
+let reconnectTimer = null
+let stopped = true
+let reconnectAttempts = 0
+let initialized = false
+let lastOrderIds = new Set()
+
+function send(data) {
+  if (socket && socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(data))
+}
+function clearTimers() {
+  if (heartbeatTimer) clearInterval(heartbeatTimer)
+  if (reconnectTimer) clearTimeout(reconnectTimer)
+  heartbeatTimer = null
+  reconnectTimer = null
+}
+function scheduleReconnect() {
+  if (stopped || reconnectTimer) return
+  const delay = Math.min(30000, 5000 * Math.pow(2, reconnectAttempts++))
+  reconnectTimer = setTimeout(() => { reconnectTimer = null; connect() }, delay)
+}
+function notifyNewOrders(items) {
+  const newItems = items.filter(item => {
+    const id = String(item.orderId || item.orderNo || '')
+    return id && !lastOrderIds.has(id)
+  })
+  lastOrderIds = new Set(items.map(item => String(item.orderId || item.orderNo || '')).filter(Boolean))
+  if (initialized && newItems.length) {
+    const first = newItems[0]
+    ElNotification({
+      title: '工单提醒',
+      message: newItems.length === 1 ? `${first.orderNo || '新工单'}:${first.orderDesc || '有新的待处理工单'}` : `收到 ${newItems.length} 条新的待处理工单`,
+      type: 'warning',
+      duration: 5000
+    })
+  }
+  initialized = true
+}
+function startHeartbeat() {
+  if (heartbeatTimer) clearInterval(heartbeatTimer)
+  heartbeatTimer = setInterval(() => send({ type: 'ping' }), 25000)
+}
+function connect() {
+  if (stopped || (socket && [WebSocket.CONNECTING, WebSocket.OPEN].includes(socket.readyState))) return
+  const rawToken = getToken()
+  if (!rawToken) { workOrderWsState.value = 'waiting-token'; scheduleReconnect(); return }
+  clearTimers()
+  workOrderWsState.value = 'connecting'
+  try {
+    socket = new WebSocket(import.meta.env.VITE_DEVICE_STATUS_WS_URL || DEFAULT_URL)
+    socket.onopen = () => {
+      reconnectAttempts = 0
+      const token = rawToken.startsWith('Bearer ') ? rawToken : `Bearer ${rawToken}`
+      send({ type: 'authenticate', token })
+    }
+    socket.onmessage = event => {
+      let message
+      try { message = JSON.parse(event.data) } catch { return }
+      if (message.type === 'authenticated') {
+        if (!message.success) { workOrderWsState.value = 'auth-failed'; stopped = true; socket.close(); return }
+        workOrderWsState.value = 'open'
+        send({ type: 'workOrderReminder' })
+        startHeartbeat()
+        return
+      }
+      if (message.type === 'workOrderReminderPush' || message.type === 'workOrderReminder') {
+        if (!message.success) return
+        const data = message.data || {}
+        const items = Array.isArray(data.items) ? data.items : []
+        workOrders.value = items
+        workOrderTotal.value = Number(data.total || 0)
+        notifyNewOrders(items)
+      }
+    }
+    socket.onerror = () => { workOrderWsState.value = 'error' }
+    socket.onclose = () => {
+      clearTimers(); socket = null
+      if (!stopped) { workOrderWsState.value = 'closed'; scheduleReconnect() }
+    }
+  } catch (error) {
+    workOrderWsState.value = 'error'; socket = null; scheduleReconnect()
+  }
+}
+export function startWorkOrderReminder() {
+  stopped = false
+  connect()
+  return { workOrders, workOrderTotal, workOrderWsState }
+}
+export function stopWorkOrderReminder() {
+  stopped = true
+  clearTimers()
+  if (socket) socket.close(1000, 'application stop')
+  socket = null
+  workOrderWsState.value = 'closed'
+}

+ 3 - 1
src/utils/request.js

@@ -100,7 +100,9 @@ service.interceptors.response.use(res => {
     }
       return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
     } else if (code === 500) {
-      ElMessage({ message: msg, type: 'error' })
+      if (!res.config.headers['X-Silent-Error']) {
+        ElMessage({ message: msg, type: 'error' })
+      }
       return Promise.reject(new Error(msg))
     } else if (code === 601) {
       ElMessage({ message: msg, type: 'warning' })

+ 18 - 33
src/views/login.vue

@@ -3,52 +3,28 @@
     <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
       <h3 class="title">沅陵太常片区城市地下管网管控系统</h3>
       <el-form-item prop="username">
-        <el-input
-          v-model="loginForm.username"
-          type="text"
-          size="large"
-          auto-complete="off"
-          placeholder="账号"
-        >
+        <el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
           <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
         </el-input>
       </el-form-item>
       <el-form-item prop="password">
-        <el-input
-          v-model="loginForm.password"
-          type="password"
-          size="large"
-          auto-complete="off"
-          placeholder="密码"
-          @keyup.enter="handleLogin"
-        >
+        <el-input v-model="loginForm.password" type="password" size="large" auto-complete="off" placeholder="密码"
+          @keyup.enter="handleLogin">
           <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
         </el-input>
       </el-form-item>
       <el-form-item prop="code" v-if="captchaEnabled">
-        <el-input
-          v-model="loginForm.code"
-          size="large"
-          auto-complete="off"
-          placeholder="验证码"
-          style="width: 63%"
-          @keyup.enter="handleLogin"
-        >
+        <el-input v-model="loginForm.code" size="large" auto-complete="off" placeholder="验证码" style="width: 63%"
+          @keyup.enter="handleLogin">
           <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
         </el-input>
         <div class="login-code">
-          <img :src="codeUrl" @click="getCode" class="login-code-img"/>
+          <img :src="codeUrl" @click="getCode" class="login-code-img" />
         </div>
       </el-form-item>
       <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
       <el-form-item style="width:100%;">
-        <el-button
-          :loading="loading"
-          size="large"
-          type="primary"
-          style="width:100%;"
-          @click.prevent="handleLogin"
-        >
+        <el-button :loading="loading" size="large" type="primary" style="width:100%;" @click.prevent="handleLogin">
           <span v-if="!loading">登 录</span>
           <span v-else>登 录 中...</span>
         </el-button>
@@ -77,7 +53,7 @@ const { proxy } = getCurrentInstance();
 
 const loginForm = ref({
   username: "admin",
-  password: "admin123",
+  password: "Admin@123",
   rememberMe: false,
   code: "",
   uuid: ""
@@ -98,7 +74,7 @@ const register = ref(false);
 const redirect = ref(undefined);
 
 watch(route, (newRoute) => {
-    redirect.value = newRoute.query && newRoute.query.redirect;
+  redirect.value = newRoute.query && newRoute.query.redirect;
 }, { immediate: true });
 
 function handleLogin() {
@@ -173,6 +149,7 @@ getCookie();
   background-size: cover;
   padding-right: 200px;
 }
+
 .title {
   margin: 0px auto 30px auto;
   text-align: center;
@@ -184,32 +161,39 @@ getCookie();
   background: #ffffff;
   width: 400px;
   padding: 25px 25px 5px 25px;
+
   .el-input {
     height: 40px;
+
     input {
       height: 40px;
     }
   }
+
   .input-icon {
     height: 39px;
     width: 14px;
     margin-left: 0px;
   }
 }
+
 .login-tip {
   font-size: 13px;
   text-align: center;
   color: #bfbfbf;
 }
+
 .login-code {
   width: 33%;
   height: 40px;
   float: right;
+
   img {
     cursor: pointer;
     vertical-align: middle;
   }
 }
+
 .el-login-footer {
   height: 40px;
   line-height: 40px;
@@ -222,6 +206,7 @@ getCookie();
   font-size: 12px;
   letter-spacing: 1px;
 }
+
 .login-code-img {
   height: 40px;
   padding-left: 12px;

+ 47 - 69
src/views/subSystem/basic/GasThreshold.vue

@@ -6,18 +6,10 @@
         <el-form-item label="设备编码">
           <el-input v-model="queryParams.deviceCode" placeholder="请输入设备编码" clearable style="width: 180px" />
         </el-form-item>
-        <el-form-item label="预警类型">
-          <el-select v-model="queryParams.warningType" placeholder="请选择" clearable style="width: 160px">
-            <el-option v-for="opt in WARNING_TYPE_OPTIONS" :key="opt.warningType" :label="opt.warningType" :value="opt.warningType" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="预警编码">
-          <el-input v-model="queryParams.warningCode" placeholder="请输入预警编码" clearable style="width: 180px" />
-        </el-form-item>
         <el-form-item>
           <el-button type="primary" @click="handleQuery">查询</el-button>
           <el-button @click="resetQuery">重置</el-button>
-          <el-button type="success" @click="handleAdd">新增阈值</el-button>
+          <el-button type="success" @click="handleAdd">按设备类型批量修改阈值</el-button>
         </el-form-item>
       </el-form>
     </el-card>
@@ -28,24 +20,22 @@
         <el-table-column label="设备编码" prop="deviceCode" min-width="140" show-overflow-tooltip />
         <el-table-column label="设备名称" min-width="140" show-overflow-tooltip>
           <template #default="{ row }">
-            {{ getDeviceName(row.deviceCode) }}
+            {{ row.equipmentName || getDeviceName(row.deviceCode) }}
           </template>
         </el-table-column>
         <el-table-column label="设备类型" min-width="120" show-overflow-tooltip>
           <template #default="{ row }">
-            {{ getDeviceType(row.deviceCode) }}
+            {{ row.equipmentTypeName || getDeviceType(row.deviceCode) }}
           </template>
         </el-table-column>
-        <el-table-column label="预警类型" prop="warningType" min-width="120" />
-        <el-table-column label="预警编码" prop="warningCode" min-width="140" />
         <el-table-column label="最小值" prop="minValue" width="90" align="center" />
         <el-table-column label="最大值" prop="maxValue" width="90" align="center" />
         <el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
-        <el-table-column label="创建时间" prop="createTime" width="170" />
         <el-table-column label="操作" width="160" align="center" fixed="right">
           <template #default="{ row }">
-            <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
-            <el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
+            <el-button v-if="row.id" type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
+            <span v-else class="muted">未配置</span>
+            <el-button v-if="row.id" type="danger" size="small" @click="handleDelete(row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -60,20 +50,11 @@
     <!-- 新增/编辑对话框 -->
     <el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px" destroy-on-close>
       <el-form :model="formData" :rules="rules" ref="formRef" label-width="100px">
-        <el-form-item label="选择设备" prop="deviceCode">
-          <el-select v-model="formData.deviceCode" placeholder="请选择设备" filterable clearable style="width: 100%"
-            @click="loadGasDeviceOptions">
-            <el-option v-for="d in deviceOptions" :key="d.equipmentCode" :label="d.equipmentCode + ' - ' + d.equipmentName" :value="d.equipmentCode" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="预警类型" prop="warningType">
-          <el-select v-model="formData.warningType" placeholder="请选择预警类型" style="width: 100%" @change="onWarningTypeChange">
-            <el-option v-for="opt in WARNING_TYPE_OPTIONS" :key="opt.warningType" :label="opt.warningType" :value="opt.warningType" />
+        <el-form-item label="设备类型" prop="deviceType">
+          <el-select v-model="formData.deviceType" placeholder="请选择设备类型" filterable clearable style="width: 100%" @change="onDeviceTypeChange">
+            <el-option v-for="type in deviceTypeOptions" :key="type" :label="type" :value="type" />
           </el-select>
         </el-form-item>
-        <el-form-item label="预警编码">
-          <el-input v-model="formData.warningCode" placeholder="选择预警类型后自动带出" />
-        </el-form-item>
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="最小值" prop="minValue">
@@ -105,21 +86,12 @@ import { getWarningThresholdModulePage, saveWarningThreshold, updateWarningThres
 
 const GAS_MODULE_TYPE = '燃气'
 
-// ==================== 燃气模块预警类型与预警编码参考 ====================
-const WARNING_TYPE_OPTIONS = [
-  { warningType: '可燃气体浓度预警', warningCode: 'WARN-GAS-CONCENTRATION' },
-  { warningType: '可燃气体温度预警', warningCode: 'WARN-GAS-TEMPERATURE' },
-  { warningType: '可燃气体湿度预警', warningCode: 'WARN-GAS-HUMIDITY' },
-  { warningType: '压力预警', warningCode: 'WARN-PRESSURE' },
-  { warningType: '温度预警', warningCode: 'WARN-TEMPERATURE' },
-  { warningType: '湿度预警', warningCode: 'WARN-HUMIDITY' }
-]
+const deviceTypeOptions = ref([])
+const filteredDeviceOptions = ref([])
 
 // ==================== 搜索参数 ====================
 const queryParams = reactive({
-  deviceCode: '',
-  warningType: '',
-  warningCode: ''
+  deviceCode: ''
 })
 const queryRef = ref(null)
 
@@ -129,8 +101,6 @@ function handleQuery() {
 }
 function resetQuery() {
   queryParams.deviceCode = ''
-  queryParams.warningType = ''
-  queryParams.warningCode = ''
   pageNum.value = 1
   loadData()
 }
@@ -147,9 +117,7 @@ async function loadData() {
   try {
     const params = {}
     if (queryParams.deviceCode) params.deviceCode = queryParams.deviceCode
-    if (queryParams.warningType) params.warningType = queryParams.warningType
-    if (queryParams.warningCode) params.warningCode = queryParams.warningCode
-    const res = await getWarningThresholdModulePage(pageNum.value, pageSize.value, GAS_MODULE_TYPE, params)
+    const res = await getWarningThresholdModulePage(pageNum.value, pageSize.value, params)
     const pageData = res.data || res
     tableData.value = pageData.records || []
     total.value = pageData.total || 0
@@ -175,36 +143,40 @@ async function ensureDeviceMap() {
     const map = {}
     list.forEach(d => { map[d.equipmentCode] = d })
     deviceInfoMap.value = map
+    deviceTypeOptions.value = [...new Set(list.map(d => d.equipmentTypeName).filter(Boolean))]
+    filteredDeviceOptions.value = list
   } catch (e) { console.error('加载燃气设备列表失败', e) }
 }
 
 function getDeviceName(deviceCode) {
-  const d = deviceInfoMap.value[deviceCode]
-  return d ? d.equipmentName : deviceCode
+  return String(deviceCode || '').split(',').map(code => {
+    const d = deviceInfoMap.value[code.trim()]
+    return d ? (d.equipmentName || code.trim()) : code.trim()
+  }).filter(Boolean).join('、') || '-'
 }
 
 function getDeviceType(deviceCode) {
-  const d = deviceInfoMap.value[deviceCode]
-  return d ? (d.typeName || '-') : '-'
+  const types = String(deviceCode || '').split(',').map(code => deviceInfoMap.value[code.trim()])
+    .filter(Boolean).map(d => d.equipmentTypeName).filter(Boolean)
+  return [...new Set(types)].join('、') || '-'
 }
 
 // ==================== 新增/编辑 ====================
 const dialogVisible = ref(false)
-const dialogTitle = ref('新增阈值')
+const dialogTitle = ref('按设备类型批量修改阈值')
 const formRef = ref(null)
 const submitLoading = ref(false)
 const isEdit = ref(false)
 
 const rules = {
-  deviceCode: [{ required: true, message: '请选择设备', trigger: 'change' }],
-  warningType: [{ required: true, message: '请选择预警类型', trigger: 'change' }]
+  deviceType: [{ required: true, message: '请选择设备类型', trigger: 'change' }]
 }
 
 const formData = reactive({
   id: '',
+  deviceType: '',
+  deviceCodes: [],
   deviceCode: '',
-  warningType: '',
-  warningCode: '',
   minValue: null,
   maxValue: null,
   remark: ''
@@ -217,6 +189,8 @@ async function loadGasDeviceOptions() {
   try {
     const res = await getEquipmentByTopLevelType(GAS_MODULE_TYPE)
     deviceOptions.value = res.data || []
+    deviceTypeOptions.value = [...new Set(deviceOptions.value.map(d => d.equipmentTypeName).filter(Boolean))]
+    filteredDeviceOptions.value = deviceOptions.value
   } catch (e) {
     console.error('加载燃气设备列表失败', e)
   }
@@ -224,20 +198,19 @@ async function loadGasDeviceOptions() {
 
 function resetForm() {
   formData.id = ''
+  formData.deviceType = ''
+  formData.deviceCodes = []
   formData.deviceCode = ''
-  formData.warningType = ''
-  formData.warningCode = ''
   formData.minValue = null
   formData.maxValue = null
   formData.remark = ''
   isEdit.value = false
-  dialogTitle.value = '新增阈值'
+  dialogTitle.value = '按设备类型批量修改阈值'
 }
 
-// 选择预警类型后自动带出预警编码
-function onWarningTypeChange(val) {
-  const opt = WARNING_TYPE_OPTIONS.find(o => o.warningType === val)
-  formData.warningCode = opt ? opt.warningCode : ''
+function onDeviceTypeChange(type) {
+  filteredDeviceOptions.value = deviceOptions.value.filter(d => d.equipmentTypeName === type)
+  formData.deviceCodes = filteredDeviceOptions.value.map(d => d.equipmentCode)
 }
 
 async function handleAdd() {
@@ -246,13 +219,16 @@ async function handleAdd() {
   dialogVisible.value = true
 }
 
-function handleEdit(row) {
+async function handleEdit(row) {
+  await loadGasDeviceOptions()
   isEdit.value = true
   dialogTitle.value = '编辑阈值'
   formData.id = row.id || ''
   formData.deviceCode = row.deviceCode
-  formData.warningType = row.warningType
-  formData.warningCode = row.warningCode || ''
+  formData.deviceCodes = String(row.deviceCode || '').split(',').map(v => v.trim()).filter(Boolean)
+  const firstDevice = deviceOptions.value.find(d => formData.deviceCodes.includes(d.equipmentCode))
+  formData.deviceType = firstDevice?.equipmentTypeName || ''
+  onDeviceTypeChange(formData.deviceType)
   formData.minValue = row.minValue
   formData.maxValue = row.maxValue
   formData.remark = row.remark || ''
@@ -262,12 +238,14 @@ function handleEdit(row) {
 async function handleSave() {
   const valid = await formRef.value.validate().catch(() => false)
   if (!valid) return
+  if (!formData.deviceCodes.length) {
+    ElMessage.warning('该设备类型下没有可配置的设备')
+    return
+  }
   submitLoading.value = true
   try {
     const data = {
-      deviceCode: formData.deviceCode,
-      warningType: formData.warningType,
-      warningCode: formData.warningCode || undefined,
+      deviceCode: formData.deviceCodes.join(','),
       minValue: formData.minValue,
       maxValue: formData.maxValue,
       remark: formData.remark || undefined
@@ -275,10 +253,10 @@ async function handleSave() {
     if (isEdit.value) {
       data.id = formData.id
       await updateWarningThreshold(data, GAS_MODULE_TYPE)
-      ElMessage.success('修改成功')
+      ElMessage.success('设备类型阈值批量修改成功')
     } else {
       await saveWarningThreshold(data, GAS_MODULE_TYPE)
-      ElMessage.success('新增成功')
+      ElMessage.success('设备类型阈值批量配置成功')
     }
     dialogVisible.value = false
     loadData()

+ 82 - 13
src/views/subSystem/drainage/fxpg/zhfxtjfx.vue

@@ -57,6 +57,15 @@
           </div>
         </div>
       </el-card>
+      <el-card class="stat-card stat-purple" shadow="hover">
+        <div class="stat-card-inner">
+          <div class="stat-icon"><el-icon :size="32"><TrendCharts /></el-icon></div>
+          <div class="stat-info">
+            <div class="stat-value">{{ stats.highRisk }}</div>
+            <div class="stat-label">高风险管网</div>
+          </div>
+        </div>
+      </el-card>
     </div>
 
     <!-- 数据表格 -->
@@ -80,6 +89,16 @@
           <span :class="row._alarmCount > 0 ? 'alarm-has' : ''">{{ row._alarmCount ?? '-' }}</span>
         </template>
       </el-table-column>
+      <el-table-column label="风险等级" min-width="100" align="center">
+        <template #default="{ row }">
+          <el-tag :type="riskTagType(row._riskLevel)" effect="light">{{ riskLevelName(row._riskLevel) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="风险评分" min-width="100" align="center">
+        <template #default="{ row }">
+          <span :class="riskScoreClass(row._riskScore)">{{ row._riskScore }}</span>
+        </template>
+      </el-table-column>
       <el-table-column label="创建时间" prop="createTime" min-width="160" align="center" />
       <el-table-column label="操作" width="100" align="center" fixed="right">
         <template #default="{ row }">
@@ -114,6 +133,10 @@
                 <el-tag :type="networkTypeTagType(currentRow.networkType)" effect="light">{{ currentRow.networkType }}</el-tag>
               </el-descriptions-item>
               <el-descriptions-item label="关联设备">{{ currentRow.equipmentId ? '已关联 (ID: ' + currentRow.equipmentId + ')' : '未关联' }}</el-descriptions-item>
+              <el-descriptions-item label="综合风险等级">
+                <el-tag :type="riskTagType(currentRow._riskLevel)" effect="light">{{ riskLevelName(currentRow._riskLevel) }}</el-tag>
+              </el-descriptions-item>
+              <el-descriptions-item label="综合风险评分">{{ currentRow._riskScore }} 分</el-descriptions-item>
               <el-descriptions-item label="坐标信息" :span="2">{{ formatPoints(currentRow.points) }}</el-descriptions-item>
               <el-descriptions-item label="创建时间">{{ currentRow.createTime }}</el-descriptions-item>
               <el-descriptions-item label="更新时间">{{ currentRow.updateTime }}</el-descriptions-item>
@@ -154,7 +177,7 @@
 <script setup name="Zhfxtjfx">
 import { ref, computed, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
-import { DataLine, Warning, WarningFilled, CircleCheck } from '@element-plus/icons-vue'
+import { DataLine, Warning, WarningFilled, CircleCheck, TrendCharts } from '@element-plus/icons-vue'
 import { getPipeNetworkPage, getAlarmDataPage } from '@/api/drainage'
 
 const router = useRouter()
@@ -184,6 +207,36 @@ const resetQuery = () => {
   loadData()
 }
 
+const getRiskScore = (item, alarmCount = 0) => {
+  // 评分完全由当前排水管网基础资料和实时报警数量计算,不使用演示数据。
+  const alarmScore = Math.min(70, Number(alarmCount || 0) * 10)
+  const equipmentScore = item.equipmentId ? 0 : 15
+  const coordinateScore = item.points ? 0 : 15
+  return Math.min(100, alarmScore + equipmentScore + coordinateScore)
+}
+
+const getRiskLevel = score => score >= 70 ? 'high' : score >= 40 ? 'medium' : 'low'
+const riskLevelName = level => level === 'high' ? '高风险' : level === 'medium' ? '中风险' : '低风险'
+const riskTagType = level => level === 'high' ? 'danger' : level === 'medium' ? 'warning' : 'success'
+const riskScoreClass = score => score >= 70 ? 'risk-score-high' : score >= 40 ? 'risk-score-medium' : 'risk-score-low'
+
+const getEquipmentIds = equipmentId => String(equipmentId || '').split(',').map(id => id.trim()).filter(Boolean)
+
+const getAlarmCountByEquipment = async equipmentId => {
+  const ids = getEquipmentIds(equipmentId)
+  if (!ids.length) return 0
+  const counts = await Promise.all(ids.map(async id => {
+    try {
+      const alarmRes = await getAlarmDataPage(1, 1, { equipmentId: id, equipmentType: 'drainage' })
+      const page = alarmRes.code !== undefined ? alarmRes.data : alarmRes
+      return Number(page?.total || 0)
+    } catch (e) {
+      return 0
+    }
+  }))
+  return counts.reduce((sum, count) => sum + count, 0)
+}
+
 // ==================== 加载数据 ====================
 const loadData = async () => {
   loading.value = true
@@ -202,15 +255,9 @@ const loadData = async () => {
     // 为每条管网查询报警数量
     const withAlarms = await Promise.all(
       rows.map(async (item) => {
-        let alarmCount = 0
-        if (item.equipmentId) {
-          try {
-            const alarmRes = await getAlarmDataPage(1, 1, { equipmentId: item.equipmentId, equipmentType: 'drainage' })
-            const alarmPageData = alarmRes.code !== undefined ? alarmRes.data : alarmRes
-            alarmCount = alarmPageData.total ?? 0
-          } catch (e) { /* ignore */ }
-        }
-        return { ...item, _alarmCount: alarmCount }
+        const alarmCount = await getAlarmCountByEquipment(item.equipmentId)
+        const riskScore = getRiskScore(item, alarmCount)
+        return { ...item, _alarmCount: alarmCount, _riskScore: riskScore, _riskLevel: getRiskLevel(riskScore) }
       })
     )
     tableData.value = withAlarms
@@ -227,11 +274,17 @@ const stats = computed(() => {
   const list = tableData.value
   const withAlarm = list.filter(i => i._alarmCount > 0).length
   const totalAlarms = list.reduce((sum, i) => sum + (i._alarmCount || 0), 0)
+  const highRisk = list.filter(i => i._riskLevel === 'high').length
+  const mediumRisk = list.filter(i => i._riskLevel === 'medium').length
+  const lowRisk = list.filter(i => i._riskLevel === 'low').length
   return {
     total: total.value,
     withAlarm,
     totalAlarms,
-    noAlarm: list.length - withAlarm
+    noAlarm: list.length - withAlarm,
+    highRisk,
+    mediumRisk,
+    lowRisk
   }
 })
 
@@ -270,8 +323,13 @@ const handleDetail = async (row) => {
   if (row.equipmentId) {
     alarmLoading.value = true
     try {
-      const res = await getAlarmDataPage(1, 20, { equipmentId: row.equipmentId, equipmentType: 'drainage' })
-      alarmList.value = res.rows || res.data?.rows || res.data || []
+      const ids = getEquipmentIds(row.equipmentId)
+      const results = await Promise.all(ids.map(id => getAlarmDataPage(1, 20, { equipmentId: id, equipmentType: 'drainage' })))
+      alarmList.value = results.flatMap(res => {
+        const page = res.code !== undefined ? res.data : res
+        if (Array.isArray(page)) return page
+        return page?.records || page?.rows || []
+      }).sort((a, b) => String(b.alarmTime || '').localeCompare(String(a.alarmTime || '')))
     } catch (e) {
       alarmList.value = []
     } finally {
@@ -364,6 +422,13 @@ onMounted(() => {
 .stat-orange .stat-value { color: #E6A23C; }
 .stat-green .stat-value { color: #67C23A; }
 
+.stat-purple .stat-icon {
+  background: rgba(144, 97, 255, 0.1);
+  color: #9061ff;
+}
+
+.stat-purple .stat-value { color: #9061ff; }
+
 .stat-label {
   font-size: 14px;
   color: #909399;
@@ -376,6 +441,10 @@ onMounted(() => {
   font-weight: 700;
 }
 
+.risk-score-high { color: #F56C6C; font-weight: 700; }
+.risk-score-medium { color: #E6A23C; font-weight: 700; }
+.risk-score-low { color: #67C23A; font-weight: 700; }
+
 /* 分页 */
 .pagination-wrap {
   display: flex;

+ 55 - 19
src/views/subSystem/drainage/fxpg/zhfxxqfx.vue

@@ -27,6 +27,10 @@
               <el-tag size="small" :type="networkTypeTagType(item.networkType)" effect="light">{{ item.networkType }}</el-tag>
               <span class="pipe-item-area">{{ item.area }}</span>
             </div>
+            <div class="pipe-item-risk">
+              <el-tag size="small" :type="riskTagType(item._riskLevel)">{{ riskLevelName(item._riskLevel) }}</el-tag>
+              <span :class="riskScoreClass(item._riskScore)">{{ item._riskScore }}分</span>
+            </div>
             <div v-if="item._alarmCount > 0" class="pipe-item-alarm">
               <el-icon color="#F56C6C"><Warning /></el-icon>
               <span>{{ item._alarmCount }} 条报警</span>
@@ -52,6 +56,8 @@
                   <span v-if="currentPipe.equipmentId">已关联 (ID: {{ currentPipe.equipmentId }})</span>
                   <span v-else style="color:#909399">未关联</span>
                 </el-descriptions-item>
+                <el-descriptions-item label="综合风险等级"><el-tag :type="riskTagType(currentPipe._riskLevel)">{{ riskLevelName(currentPipe._riskLevel) }}</el-tag></el-descriptions-item>
+                <el-descriptions-item label="综合风险评分">{{ currentPipe._riskScore }} 分</el-descriptions-item>
                 <el-descriptions-item label="坐标信息" :span="2">{{ formatPoints(currentPipe.points) }}</el-descriptions-item>
                 <el-descriptions-item label="创建时间">{{ currentPipe.createTime }}</el-descriptions-item>
                 <el-descriptions-item label="更新时间">{{ currentPipe.updateTime }}</el-descriptions-item>
@@ -146,6 +152,14 @@ const networkTypeOptions = ['管线', '管点', '排口']
 const searchKey = ref({ name: '', networkType: '管线' })
 const handleSearch = () => { loadPipeList() }
 
+const getEquipmentIds = equipmentId => String(equipmentId || '').split(',').map(id => id.trim()).filter(Boolean)
+const getRiskScore = (item, alarmCount) => Math.min(100,
+  Math.min(70, Number(alarmCount || 0) * 10) + (item.equipmentId ? 0 : 15) + (item.points ? 0 : 15))
+const getRiskLevel = score => score >= 70 ? 'high' : score >= 40 ? 'medium' : 'low'
+const riskLevelName = level => level === 'high' ? '高风险' : level === 'medium' ? '中风险' : '低风险'
+const riskTagType = level => level === 'high' ? 'danger' : level === 'medium' ? 'warning' : 'success'
+const riskScoreClass = score => score >= 70 ? 'risk-score-high' : score >= 40 ? 'risk-score-medium' : 'risk-score-low'
+
 // ==================== 管网列表 ====================
 const pipeList = ref([])
 const listLoading = ref(false)
@@ -168,14 +182,17 @@ const loadPipeList = async () => {
     const withAlarms = await Promise.all(
       rows.map(async (item) => {
         let alarmCount = 0
-        if (item.equipmentId) {
+        const ids = getEquipmentIds(item.equipmentId)
+        const counts = await Promise.all(ids.map(async id => {
           try {
-            const alarmRes = await getAlarmDataPage(1, 1, { equipmentId: item.equipmentId, equipmentType: 'drainage' })
+            const alarmRes = await getAlarmDataPage(1, 1, { equipmentId: id, equipmentType: 'drainage' })
             const alarmPageData = alarmRes.code !== undefined ? alarmRes.data : alarmRes
-            alarmCount = alarmPageData.total ?? 0
-          } catch (e) { /* ignore */ }
-        }
-        return { ...item, _alarmCount: alarmCount }
+            return Number(alarmPageData?.total || 0)
+          } catch (e) { return 0 }
+        }))
+        alarmCount = counts.reduce((sum, count) => sum + count, 0)
+        const riskScore = getRiskScore(item, alarmCount)
+        return { ...item, _alarmCount: alarmCount, _riskScore: riskScore, _riskLevel: getRiskLevel(riskScore) }
       })
     )
     pipeList.value = withAlarms
@@ -217,17 +234,28 @@ const alarmUnhandled = computed(() => alarmList.value.filter(i => i.status === 0
 const alarmHandling = computed(() => alarmList.value.filter(i => i.status === 1).length)
 const alarmHandled = computed(() => alarmList.value.filter(i => i.status === 2).length)
 
+const queryAlarmsForPipe = async (equipmentId, pageSize) => {
+  const ids = getEquipmentIds(equipmentId)
+  if (!ids.length) return { rows: [], total: 0 }
+  const result = await Promise.all(ids.map(async id => {
+    const res = await getAlarmDataPage(1, pageSize, { equipmentId: id, equipmentType: 'drainage' })
+    const page = res.code !== undefined ? res.data : res
+    return { rows: page?.records || page?.rows || [], total: Number(page?.total || 0) }
+  }))
+  return {
+    rows: result.flatMap(item => item.rows).sort((a, b) => String(b.alarmTime || '').localeCompare(String(a.alarmTime || ''))),
+    total: result.reduce((sum, item) => sum + item.total, 0)
+  }
+}
+
 const loadAlarmData = async () => {
   if (!currentPipe.value?.equipmentId) return
   alarmLoading.value = true
   try {
-    const res = await getAlarmDataPage(alarmPageNum.value, alarmPageSize.value, {
-      equipmentId: currentPipe.value.equipmentId,
-      equipmentType: 'drainage'
-    })
-    const pageData = res.code !== undefined ? res.data : res
-    alarmList.value = pageData.records || pageData.rows || []
-    alarmTotal.value = pageData.total ?? 0
+    const result = await queryAlarmsForPipe(currentPipe.value.equipmentId, alarmPageSize.value)
+    const start = (alarmPageNum.value - 1) * alarmPageSize.value
+    alarmList.value = result.rows.slice(start, start + alarmPageSize.value)
+    alarmTotal.value = result.total
   } catch (e) {
     alarmList.value = []
     alarmTotal.value = 0
@@ -245,12 +273,8 @@ const loadAlarmTrend = async () => {
   if (!currentPipe.value?.equipmentId) return
   try {
     // 加载较多报警数据用于趋势分析
-    const res = await getAlarmDataPage(1, 500, {
-      equipmentId: currentPipe.value.equipmentId,
-      equipmentType: 'drainage'
-    })
-    const trendPageData = res.code !== undefined ? res.data : res
-    const rows = trendPageData.records || trendPageData.rows || []
+    const result = await queryAlarmsForPipe(currentPipe.value.equipmentId, 500)
+    const rows = result.rows
     // 按日期聚合报警数
     const dateMap = {}
     rows.forEach(item => {
@@ -413,6 +437,18 @@ onMounted(() => {
   color: #909399;
 }
 
+.pipe-item-risk {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-top: 6px;
+  font-size: 12px;
+}
+
+.risk-score-high { color: #F56C6C; font-weight: 700; }
+.risk-score-medium { color: #E6A23C; font-weight: 700; }
+.risk-score-low { color: #67C23A; font-weight: 700; }
+
 .pipe-item-alarm {
   display: flex;
   align-items: center;

+ 11 - 4
src/views/subSystem/drainage/jcbj/bjsh.vue

@@ -197,7 +197,7 @@
 import { ref, computed, onMounted, nextTick } from 'vue'
 import { ElMessage } from 'element-plus'
 import locationIcon from '@/assets/images/location.png'
-import { getAlarmDataPage, handleAlarm } from '@/api/drainage'
+import { approveAlarmToWorkOrder, getAlarmDataPage, handleAlarm } from '@/api/drainage'
 import useUserStore from '@/store/modules/user'
 
 // ==================== 用户信息 ====================
@@ -363,10 +363,17 @@ async function submitAudit() {
   }
   submitting.value = true
   try {
-    // 调用 handleAlarm 真实接口处理报警
-    const res = await handleAlarm(currentAlarm.value.id, userStore.name || 'admin', auditForm.value.opinion || auditForm.value.result)
+    // “确认报警/降级处理”审核通过后自动上传为运维工单;误报/解除仍只关闭报警
+    const isApproved = ['确认报警', '降级处理'].includes(auditForm.value.result)
+    const res = isApproved
+      ? await approveAlarmToWorkOrder({
+          alarmId: currentAlarm.value.id,
+          orderLevel: auditForm.value.result === '降级处理' ? 2 : currentAlarm.value.warningLevel,
+          orderDesc: auditForm.value.opinion || `${currentAlarm.value.warningType || '监测'}异常,请现场核实处理`
+        })
+      : await handleAlarm(currentAlarm.value.id, userStore.name || 'admin', auditForm.value.opinion || auditForm.value.result)
     if (res.code === 200 || res.code === 0) {
-      ElMessage.success('审核提交成功')
+      ElMessage.success(isApproved ? '审核通过,已自动上传至工单' : '审核提交成功')
       auditVisible.value = false
       loadData()
     } else {

+ 47 - 10
src/views/subSystem/drainage/jcbj/bjyp.vue

@@ -24,10 +24,20 @@
       </div>
     </div>
 
+    <!-- 统计时间范围:当前范围会同时作用于卡片、列表和全部图表 -->
+    <div class="range-bar">
+      <span class="range-label">统计时间:</span>
+      <el-radio-group v-model="activeDays" size="small" @change="handleRangeChange">
+        <el-radio-button v-for="option in rangeOptions" :key="option.value" :label="option.value">
+          {{ option.label }}
+        </el-radio-button>
+      </el-radio-group>
+    </div>
+
     <!-- 图表区域 -->
     <div class="chart-section">
       <div class="chart-left">
-        <div class="section-title">最近7天报警趋势</div>
+        <div class="section-title">最近{{ activeDays }}天报警趋势</div>
         <div ref="trendChartRef" class="trend-chart"></div>
       </div>
       <div class="chart-right">
@@ -113,6 +123,12 @@ const isAdmin = computed(() => userStore.roles && userStore.roles.includes('admi
 
 // ==================== Tab栏 ====================
 const activeTab = ref('home')
+const activeDays = ref(7)
+const rangeOptions = [
+  { label: '7天', value: 7 },
+  { label: '15天', value: 15 },
+  { label: '30天', value: 30 }
+]
 
 // ==================== 等级/状态映射 ====================
 const levelMap = { 1: '严重', 2: '重要', 3: '一般' }
@@ -228,13 +244,17 @@ const statCards = computed(() => {
 async function loadData() {
   loading.value = true
   try {
-    // 计算7天前的日期字符串
-    const sevenDaysAgo = new Date(Date.now() - 7 * 86400000)
-    const startTime = sevenDaysAgo.getFullYear() + '-' +
-      String(sevenDaysAgo.getMonth() + 1).padStart(2, '0') + '-' +
-      String(sevenDaysAgo.getDate()).padStart(2, '0')
-
-    const res = await getAlarmDataPage(1, 9999, { equipmentType: 'drainage', startTime })
+    const now = new Date()
+    const start = new Date(now.getTime() - (activeDays.value - 1) * 86400000)
+    const formatDateTime = (date) => {
+      const datePart = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
+      return `${datePart} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
+    }
+    const res = await getAlarmDataPage(1, 9999, {
+      equipmentType: 'drainage',
+      startTime: formatDateTime(start),
+      endTime: formatDateTime(now)
+    })
     const pageData = res.code !== undefined ? res.data : res
     const records = pageData.records || pageData.rows || []
 
@@ -273,6 +293,11 @@ async function loadData() {
   }
 }
 
+function handleRangeChange() {
+  pageNum.value = 1
+  loadData()
+}
+
 function updateTablePage() {
   const start = (pageNum.value - 1) * pageSize.value
   tableData.value = allRecords.value.slice(start, start + pageSize.value)
@@ -306,11 +331,11 @@ function initTrendChart() {
   if (trendChart) trendChart.dispose()
   trendChart = echarts.init(trendChartRef.value)
 
-  // 生成最近7天日期键(M/D 格式与 YYYY-MM-DD 格式)
+  // 生成当前时间范围的日期键(M/D 格式与 YYYY-MM-DD 格式)
   const days = []
   const dayKeys = []
   const now = new Date()
-  for (let i = 6; i >= 0; i--) {
+  for (let i = activeDays.value - 1; i >= 0; i--) {
     const d = new Date(now.getTime() - i * 86400000)
     days.push((d.getMonth() + 1) + '/' + d.getDate())
     dayKeys.push(d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'))
@@ -627,6 +652,18 @@ onBeforeUnmount(() => {
   margin-top: 8px;
 }
 
+.range-bar {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 12px;
+  color: rgba(255, 255, 255, 0.65);
+}
+
+.range-label {
+  font-size: 13px;
+}
+
 .trend-up {
   color: #f56c6c;
   font-weight: 600;

+ 189 - 58
src/views/subSystem/drainage/jcsj/psfqgl.vue

@@ -7,12 +7,12 @@
         <div class="map-tab" :class="{ active: mapTab === '雨水分区' }" @click="switchMapTab('雨水分区')">雨水分区</div>
         <div class="map-tab" :class="{ active: mapTab === '污水分区' }" @click="switchMapTab('污水分区')">污水分区</div>
       </div>
-      <div id="psfqglMainMap" class="map-container" style="height: 350px;"></div>
+        <div id="psfqglMainMap" class="map-container" style="height: 350px;"></div>
     </div>
 
     <!-- 搜索区域 -->
     <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
-      <el-form-item label="监测点名称">
+      <el-form-item label="分区名称">
         <el-input v-model="queryParams.name" placeholder="请输入" clearable style="width: 200px" />
       </el-form-item>
       <el-form-item label="地点">
@@ -39,7 +39,7 @@
     <el-table v-loading="loading" :data="filteredList" border style="width: 100%"
       @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="监测点名称" prop="name" min-width="160" />
+      <el-table-column label="分区名称" prop="name" min-width="160" />
       <el-table-column label="地点" prop="district" min-width="100" />
       <el-table-column label="分区类型" prop="partitionType" min-width="100" />
       <el-table-column label="数据时间" prop="time" min-width="120" />
@@ -72,7 +72,7 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <div class="info-item">
-              <span class="info-label">监测点名称:</span>
+            <span class="info-label">分区名称:</span>
               <span class="info-value">{{ currentRow.name }}</span>
             </div>
           </el-col>
@@ -128,7 +128,7 @@
       <!-- 地图区域 -->
       <div class="map-section">
         <div class="map-tabs">
-          <div class="map-tab active">位置标记</div>
+          <div class="map-tab active">分区面预览</div>
         </div>
         <div id="psfqglViewMap" class="map-container"></div>
       </div>
@@ -140,8 +140,8 @@
       <el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="100px">
         <el-row :gutter="20">
           <el-col :span="12">
-            <el-form-item label="监测点名称" prop="name">
-              <el-input v-model="editForm.name" placeholder="请输入监测点名称" />
+            <el-form-item label="分区名称" prop="name">
+              <el-input v-model="editForm.name" placeholder="请输入分区名称" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
@@ -159,7 +159,8 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="分区类型" prop="partitionType">
-              <el-select v-model="editForm.partitionType" placeholder="请选择分区类型" style="width:100%">
+              <el-select v-model="editForm.partitionType" placeholder="请选择分区类型" style="width:100%"
+                @change="handlePartitionTypeChange">
                 <el-option label="雨水分区" value="雨水分区" />
                 <el-option label="污水分区" value="污水分区" />
               </el-select>
@@ -193,12 +194,15 @@
       <!-- 地图标点区域 -->
       <div class="map-section" style="margin-top: 16px;">
         <div class="map-tabs">
-          <div class="map-tab active">位置标记</div>
-          <div class="map-tab-action" @click="clearEditMarkers">清除标记</div>
+          <div class="map-tab active">分区面绘制</div>
+          <div v-if="isPolygonPartition" class="map-tab-action" @click="togglePolygonDrawing">
+            {{ isDrawingPolygon ? '完成绘制' : '开始绘制' }}
+          </div>
+          <div class="map-tab-action" @click="clearEditMarkers">清除边界</div>
         </div>
         <div id="psfqglEditMap" class="map-container" style="height: 350px;"></div>
         <div class="marker-count">
-          经度:{{ editForm.longitude || '未标记' }},纬度:{{ editForm.latitude || '未标记' }}
+          {{ isPolygonPartition ? `已绘制 ${editPolygonPoints.length} 个点` : '请选择分区类型后绘制面' }}
         </div>
       </div>
 
@@ -219,11 +223,12 @@
         </el-form-item>
       </el-form>
 
-      <el-table ref="bindTableRef" v-loading="bindLoading" :data="bindTableData" row-key="equipmentId" border
+        <el-table ref="bindTableRef" v-loading="bindLoading" :data="bindTableData" row-key="equipmentId" border
         style="width: 100%" height="400">
         <el-table-column label="选择" width="55" align="center">
           <template #default="{ row }">
-            <el-radio v-model="selectedEquipmentId" :label="row.equipmentId">&nbsp;</el-radio>
+            <el-checkbox :model-value="selectedEquipmentIds.includes(String(row.equipmentId))"
+              @change="toggleEquipment(row, $event)" />
           </template>
         </el-table-column>
         <el-table-column label="设备名称" prop="equipmentName" min-width="160" />
@@ -283,6 +288,29 @@ let mainMapInstance = null
 const mainMapMarkers = ref([])
 const mapTab = ref('all')
 
+function parsePolygonPoints(value) {
+  if (!value) return []
+  try {
+    const points = typeof value === 'string' ? JSON.parse(value) : value
+    return Array.isArray(points)
+      ? points.map(point => ({ lng: Number(point.lng), lat: Number(point.lat) }))
+        .filter(point => Number.isFinite(point.lng) && Number.isFinite(point.lat))
+      : []
+  } catch (e) {
+    return []
+  }
+}
+
+function getPartitionCenter(item) {
+  const points = parsePolygonPoints(item.points)
+  if (points.length) {
+    const lng = points.reduce((sum, point) => sum + point.lng, 0) / points.length
+    const lat = points.reduce((sum, point) => sum + point.lat, 0) / points.length
+    return { lng, lat }
+  }
+  return { lng: Number(item.longitude), lat: Number(item.latitude) }
+}
+
 function switchMapTab(tab) {
   mapTab.value = tab
   renderMainMapMarkers()
@@ -310,16 +338,32 @@ function renderMainMapMarkers() {
   })
   mainMapMarkers.value = []
 
-  const list = tableData.value.filter(item => item.longitude && item.latitude)
+  const list = tableData.value.filter(item => parsePolygonPoints(item.points).length >= 3 || (item.longitude && item.latitude))
   const filtered = mapTab.value === 'all'
     ? list
     : list.filter(item => item.partitionType === mapTab.value)
 
   filtered.forEach(item => {
-    const bPoint = new BMapGL.Point(item.longitude, item.latitude)
     const color = item.partitionType === '污水分区' ? '#e6a23c' : '#409eff'
-    const label = createMapMarker(mainMapInstance, bPoint, item.name, color)
-    mainMapMarkers.value.push(label)
+    const points = parsePolygonPoints(item.points)
+    if (points.length >= 3) {
+      const polygon = new BMapGL.Polygon(points.map(point => new BMapGL.Point(point.lng, point.lat)), {
+        strokeColor: color,
+        fillColor: color,
+        strokeWeight: 2,
+        strokeOpacity: 0.8,
+        fillOpacity: 0.25
+      })
+      mainMapInstance.addOverlay(polygon)
+      mainMapMarkers.value.push(polygon)
+      const center = getPartitionCenter(item)
+      const label = createMapMarker(mainMapInstance, new BMapGL.Point(center.lng, center.lat), item.name, color)
+      mainMapMarkers.value.push(label)
+    } else {
+      const bPoint = new BMapGL.Point(item.longitude, item.latitude)
+      const label = createMapMarker(mainMapInstance, bPoint, item.name, color)
+      mainMapMarkers.value.push(label)
+    }
   })
 }
 
@@ -445,16 +489,20 @@ function initViewMap() {
 
   try {
     viewMapInstance = new BMapGL.Map('psfqglViewMap')
-    const centerLng = currentRow.value.longitude || 110.393
-    const centerLat = currentRow.value.latitude || 28.452
+    const center = getPartitionCenter(currentRow.value)
+    const centerLng = center.lng || 110.393
+    const centerLat = center.lat || 28.452
     const centerPoint = new BMapGL.Point(centerLng, centerLat)
     viewMapInstance.centerAndZoom(centerPoint, 15)
     viewMapInstance.enableScrollWheelZoom(true)
 
-    // 如果有坐标,显示标记
-    if (currentRow.value.longitude && currentRow.value.latitude) {
-      const bPoint = new BMapGL.Point(currentRow.value.longitude, currentRow.value.latitude)
-      createMapMarker(viewMapInstance, bPoint, currentRow.value.name)
+    const points = parsePolygonPoints(currentRow.value.points)
+    if (points.length >= 3) {
+      viewMapInstance.addOverlay(new BMapGL.Polygon(points.map(point => new BMapGL.Point(point.lng, point.lat)), {
+        strokeColor: '#409eff', fillColor: '#409eff', strokeWeight: 2, fillOpacity: 0.25
+      }))
+    } else if (currentRow.value.longitude && currentRow.value.latitude) {
+      createMapMarker(viewMapInstance, new BMapGL.Point(currentRow.value.longitude, currentRow.value.latitude), currentRow.value.name)
     }
   } catch (e) {
     console.error('百度地图初始化失败:', e)
@@ -463,7 +511,7 @@ function initViewMap() {
 
 // ==================== 新增/修改对话框 ====================
 const editDialogVisible = ref(false)
-const editDialogTitle = ref('新增监测点')
+const editDialogTitle = ref('新增排水分区')
 const editFormRef = ref(null)
 const isEditMode = ref(false)
 const editRowId = ref(null)
@@ -477,21 +525,28 @@ const editForm = ref({
   waterlogging: 0,
   level: 1,
   longitude: null,
-  latitude: null
+  latitude: null,
+  points: ''
 })
 
 const editRules = {
-  name: [{ required: true, message: '请输入监测点名称', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入分区名称', trigger: 'blur' }],
   time: [{ required: true, message: '请选择数据时间', trigger: 'change' }]
 }
 
 // 编辑弹窗地图相关
 let editMapInstance = null
 let editMarkerLabel = null
+let editPolygonOverlay = null
+const editPolygonPoints = ref([])
+const isDrawingPolygon = ref(false)
+const editPointOverlays = ref([])
+const polygonPartitionTypes = ['雨水分区', '污水分区']
+const isPolygonPartition = computed(() => polygonPartitionTypes.includes(editForm.value.partitionType))
 
 function handleAdd() {
   isEditMode.value = false
-  editDialogTitle.value = '新增监测点'
+  editDialogTitle.value = '新增排水分区'
   editRowId.value = null
   editForm.value = {
     name: '',
@@ -502,14 +557,15 @@ function handleAdd() {
     waterlogging: 0,
     level: 1,
     longitude: null,
-    latitude: null
+    latitude: null,
+    points: ''
   }
   editDialogVisible.value = true
 }
 
 function handleEdit(row) {
   isEditMode.value = true
-  editDialogTitle.value = '修改监测点'
+  editDialogTitle.value = '修改排水分区'
   editRowId.value = row.id
   editForm.value = {
     name: row.name,
@@ -520,7 +576,8 @@ function handleEdit(row) {
     waterlogging: row.waterlogging,
     level: row.level,
     longitude: row.longitude,
-    latitude: row.latitude
+    latitude: row.latitude,
+    points: row.points || ''
   }
   editDialogVisible.value = true
 }
@@ -528,9 +585,13 @@ function handleEdit(row) {
 function handleEditClose() {
   editDialogVisible.value = false
   if (editMapInstance) {
-    editMapInstance = null
+  editMapInstance = null
   }
   editMarkerLabel = null
+  editPolygonOverlay = null
+  editPointOverlays.value = []
+  editPolygonPoints.value = []
+  isDrawingPolygon.value = false
 }
 
 function onEditDialogOpened() {
@@ -551,22 +612,15 @@ function initEditMap() {
     editMapInstance.centerAndZoom(centerPoint, 15)
     editMapInstance.enableScrollWheelZoom(true)
 
-    // 如果已有坐标,显示标记
-    if (editForm.value.longitude && editForm.value.latitude) {
-      const bPoint = new BMapGL.Point(editForm.value.longitude, editForm.value.latitude)
-      editMarkerLabel = createMapMarker(editMapInstance, bPoint, editForm.value.name)
-    }
+    editPolygonPoints.value = parsePolygonPoints(editForm.value.points)
+    renderEditPolygon()
 
-    // 点击地图更新标记
     editMapInstance.addEventListener('click', function (e) {
-      editForm.value.longitude = e.latlng.lng
-      editForm.value.latitude = e.latlng.lat
-      // 清除旧标记,添加新标记
-      if (editMarkerLabel) {
-        editMapInstance.removeOverlay(editMarkerLabel)
-      }
-      const bPoint = new BMapGL.Point(e.latlng.lng, e.latlng.lat)
-      editMarkerLabel = createMapMarker(editMapInstance, bPoint, editForm.value.name)
+      if (!isDrawingPolygon.value || !isPolygonPartition.value) return
+      const point = e.latLng || e.latlng
+      if (!point) return
+      editPolygonPoints.value.push({ lng: point.lng, lat: point.lat })
+      renderEditPolygon()
     })
   } catch (e) {
     console.error('百度地图初始化失败:', e)
@@ -574,18 +628,90 @@ function initEditMap() {
 }
 
 function clearEditMarkers() {
-  if (editMapInstance && editMarkerLabel) {
-    editMapInstance.removeOverlay(editMarkerLabel)
-    editMarkerLabel = null
+  if (editMapInstance && editPolygonOverlay) editMapInstance.removeOverlay(editPolygonOverlay)
+  if (editMapInstance && editMarkerLabel) editMapInstance.removeOverlay(editMarkerLabel)
+  if (editMapInstance) {
+    editPointOverlays.value.forEach(overlay => editMapInstance.removeOverlay(overlay))
   }
+  editPolygonOverlay = null
+  editMarkerLabel = null
+  editPointOverlays.value = []
+  editPolygonPoints.value = []
+  editForm.value.points = ''
   editForm.value.longitude = null
   editForm.value.latitude = null
+  isDrawingPolygon.value = false
+}
+
+function handlePartitionTypeChange(type) {
+  if (!polygonPartitionTypes.includes(type)) clearEditMarkers()
+}
+
+function renderEditPolygon() {
+  if (!editMapInstance) return
+  if (editPolygonOverlay) editMapInstance.removeOverlay(editPolygonOverlay)
+  editPointOverlays.value.forEach(overlay => editMapInstance.removeOverlay(overlay))
+  editPointOverlays.value = []
+  editPolygonPoints.value.forEach(point => {
+    const overlay = new BMapGL.Circle(new BMapGL.Point(point.lng, point.lat), 5, {
+      strokeColor: '#ffffff',
+      strokeWeight: 2,
+      strokeOpacity: 1,
+      fillColor: '#409eff',
+      fillOpacity: 1
+    })
+    editMapInstance.addOverlay(overlay)
+    editPointOverlays.value.push(overlay)
+  })
+  if (editPolygonPoints.value.length < 2) return
+  const points = editPolygonPoints.value.map(point => new BMapGL.Point(point.lng, point.lat))
+  const OverlayType = editPolygonPoints.value.length >= 3 ? BMapGL.Polygon : BMapGL.Polyline
+  editPolygonOverlay = new OverlayType(points, {
+    strokeColor: '#409eff',
+    strokeWeight: 2,
+    strokeOpacity: 0.9,
+    fillColor: '#409eff',
+    fillOpacity: 0.25
+  })
+  editMapInstance.addOverlay(editPolygonOverlay)
+}
+
+function togglePolygonDrawing() {
+  if (!isPolygonPartition.value) return
+  if (isDrawingPolygon.value && editPolygonPoints.value.length < 3) {
+    ElMessage.warning('分区面至少需要绘制3个点')
+    return
+  }
+  isDrawingPolygon.value = !isDrawingPolygon.value
+  if (!isDrawingPolygon.value) finalizePolygon()
+}
+
+function finalizePolygon() {
+  if (editPolygonPoints.value.length < 3) return false
+  const points = editPolygonPoints.value.map(point => ({
+    lng: Number(point.lng.toFixed(8)),
+    lat: Number(point.lat.toFixed(8))
+  }))
+  editPolygonPoints.value = points
+  editForm.value.points = JSON.stringify(points)
+  editForm.value.longitude = points.reduce((sum, point) => sum + point.lng, 0) / points.length
+  editForm.value.latitude = points.reduce((sum, point) => sum + point.lat, 0) / points.length
+  renderEditPolygon()
+  return true
 }
 
 function handleEditSubmit() {
   editFormRef.value.validate(async valid => {
     if (!valid) return
 
+    if (isPolygonPartition.value) {
+      if (isDrawingPolygon.value) finalizePolygon()
+      if (editPolygonPoints.value.length < 3) {
+        ElMessage.warning('分区面至少需要在地图上绘制3个点')
+        return
+      }
+    }
+
     const formData = {
       ...editForm.value
     }
@@ -674,18 +800,15 @@ const bindTotal = ref(0)
 const bindPageNum = ref(1)
 const bindPageSize = ref(10)
 const bindLoading = ref(false)
-const selectedEquipmentId = ref('')
+const selectedEquipmentIds = ref([])
 async function handleBind(row) {
   currentPartitionId.value = row.id
   bindDialogVisible.value = true
   bindPageNum.value = 1
   bindQueryParams.value.equipmentName = ''
-  // 回显已关联设备(取第一个)
-  if (row.equipmentId) {
-    selectedEquipmentId.value = row.equipmentId.split(',')[0].trim()
-  } else {
-    selectedEquipmentId.value = ''
-  }
+  selectedEquipmentIds.value = row.equipmentId
+    ? row.equipmentId.split(',').map(id => id.trim()).filter(Boolean)
+    : []
   nextTick(() => {
     loadBindData()
   })
@@ -713,18 +836,26 @@ async function loadBindData() {
 function closeBindDialog() {
   bindDialogVisible.value = false
   currentPartitionId.value = null
-  selectedEquipmentId.value = ''
+  selectedEquipmentIds.value = []
+}
+
+function toggleEquipment(row, checked) {
+  const id = String(row.equipmentId)
+  const ids = new Set(selectedEquipmentIds.value)
+  if (checked) ids.add(id)
+  else ids.delete(id)
+  selectedEquipmentIds.value = [...ids]
 }
 
 async function submitBind() {
-  if (!selectedEquipmentId.value) {
+  if (!selectedEquipmentIds.value.length) {
     ElMessage.warning('请选择要关联的设备')
     return
   }
   try {
     const res = await bindPartitionEquipment({
       partitionId: currentPartitionId.value,
-      equipmentIds: [selectedEquipmentId.value]
+      equipmentIds: selectedEquipmentIds.value
     })
     if (res.code === 0 || res.code === 200) {
       ElMessage.success('关联成功')

+ 33 - 13
src/views/subSystem/drainage/jcsj/psgwgl.vue

@@ -200,10 +200,11 @@
           <el-button type="primary" @click="handleBindQuery">搜索</el-button>
         </el-form-item>
       </el-form>
-      <el-table v-loading="bindLoading" :data="bindTableData" border row-key="equipmentId" ref="bindTableRef">
+      <el-table v-loading="bindLoading" :data="bindTableData" border row-key="equipmentId">
         <el-table-column label="选择" width="55" align="center">
           <template #default="{ row }">
-            <el-radio v-model="selectedEquipmentId" :label="row.equipmentId">&nbsp;</el-radio>
+            <el-checkbox :model-value="selectedEquipmentIds.includes(String(row.equipmentId))"
+              :disabled="isEquipmentBound(row)" @change="toggleEquipment(row, $event)" />
           </template>
         </el-table-column>
         <el-table-column label="设备名称" prop="equipmentName" min-width="120" />
@@ -233,7 +234,8 @@ import {
   deleteDrainagePipeNetwork,
   getEquipmentPage,
   getEquipmentById,
-  bindPipeNetworkEquipment
+  bindPipeNetworkEquipment,
+  getPipeNetworkBoundEquipmentIds
 } from '@/api/drainage'
 
 // 地图标记创建(用Label+img代替Marker)
@@ -779,7 +781,8 @@ const bindTotal = ref(0)
 const bindPageNum = ref(1)
 const bindPageSize = ref(10)
 const bindQueryParams = ref({ equipmentName: '', equipmentTypeId: 'drainage' })
-const selectedEquipmentId = ref('')
+const selectedEquipmentIds = ref([])
+const boundEquipmentMap = ref({})
 const currentPartitionId = ref('')
 const bindTableRef = ref(null)
 
@@ -788,12 +791,10 @@ async function handleBind(row) {
   bindDialogVisible.value = true
   bindPageNum.value = 1
   bindQueryParams.value.equipmentName = ''
-  // 回显已关联设备(取第一个)
-  if (row.equipmentId) {
-    selectedEquipmentId.value = row.equipmentId.split(',')[0].trim()
-  } else {
-    selectedEquipmentId.value = ''
-  }
+  // 回显当前监测点的全部设备;其他监测点已占用的设备不可再次选择。
+  selectedEquipmentIds.value = row.equipmentId
+    ? String(row.equipmentId).split(',').map(id => id.trim()).filter(Boolean)
+    : []
   nextTick(() => {
     loadBindData()
   })
@@ -811,6 +812,9 @@ async function loadBindData() {
     const pageData = res.code !== undefined ? res.data : res
     bindTableData.value = pageData.records || []
     bindTotal.value = pageData.total || 0
+    const boundRes = await getPipeNetworkBoundEquipmentIds(currentPartitionId.value)
+    const boundData = boundRes.code !== undefined ? boundRes.data : boundRes
+    boundEquipmentMap.value = boundData && typeof boundData === 'object' ? boundData : {}
   } catch (e) {
     console.error('加载设备列表失败', e)
   } finally {
@@ -821,18 +825,34 @@ async function loadBindData() {
 function closeBindDialog() {
   bindDialogVisible.value = false
   bindTableData.value = []
-  selectedEquipmentId.value = ''
+  selectedEquipmentIds.value = []
+  boundEquipmentMap.value = {}
+}
+
+function isEquipmentBound(row) {
+  const id = String(row.equipmentId)
+  return Boolean(boundEquipmentMap.value[id]) && !selectedEquipmentIds.value.includes(id)
+}
+
+function toggleEquipment(row, checked) {
+  const id = String(row.equipmentId)
+  if (isEquipmentBound(row)) return
+  if (checked) {
+    if (!selectedEquipmentIds.value.includes(id)) selectedEquipmentIds.value.push(id)
+  } else {
+    selectedEquipmentIds.value = selectedEquipmentIds.value.filter(item => item !== id)
+  }
 }
 
 async function submitBind() {
-  if (!selectedEquipmentId.value) {
+  if (selectedEquipmentIds.value.length === 0) {
     ElMessage.warning('请选择要关联的设备')
     return
   }
   try {
     const res = await bindPipeNetworkEquipment({
       partitionId: currentPartitionId.value,
-      equipmentIds: [selectedEquipmentId.value]
+      equipmentIds: selectedEquipmentIds.value
     })
     if (res.code === 0 || res.code === 200) {
       ElMessage.success('关联成功')

+ 1 - 0
src/views/subSystem/drainage/jcsj/sbyx.vue

@@ -895,6 +895,7 @@ function handleWarningSubmit() {
   margin-bottom: 16px;
 }
 
+
 /* 详情弹窗 */
 .detail-info {
   margin-bottom: 16px;

+ 56 - 85
src/views/subSystem/drainage/jcyj/DrainageThreshold.vue

@@ -6,18 +6,10 @@
         <el-form-item label="设备编码">
           <el-input v-model="queryParams.deviceCode" placeholder="请输入设备编码" clearable style="width: 180px" />
         </el-form-item>
-        <el-form-item label="预警类型">
-          <el-select v-model="queryParams.warningType" placeholder="请选择" clearable style="width: 160px">
-            <el-option v-for="opt in WARNING_TYPE_OPTIONS" :key="opt.warningType" :label="opt.warningType" :value="opt.warningType" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="预警编码">
-          <el-input v-model="queryParams.warningCode" placeholder="请输入预警编码" clearable style="width: 180px" />
-        </el-form-item>
         <el-form-item>
           <el-button type="primary" @click="handleQuery">查询</el-button>
           <el-button @click="resetQuery">重置</el-button>
-          <el-button type="success" @click="handleAdd">新增阈值</el-button>
+          <el-button type="success" @click="handleAdd">按设备类型批量修改阈值</el-button>
         </el-form-item>
       </el-form>
     </el-card>
@@ -28,24 +20,23 @@
         <el-table-column label="设备编码" prop="deviceCode" min-width="140" show-overflow-tooltip />
         <el-table-column label="设备名称" min-width="140" show-overflow-tooltip>
           <template #default="{ row }">
-            {{ getDeviceName(row.deviceCode) }}
+            {{ row.equipmentName || getDeviceName(row.deviceCode) }}
           </template>
         </el-table-column>
         <el-table-column label="设备类型" min-width="120" show-overflow-tooltip>
           <template #default="{ row }">
-            {{ getDeviceType(row.deviceCode) }}
+            {{ row.equipmentTypeName || getDeviceType(row.deviceCode) }}
           </template>
         </el-table-column>
-        <el-table-column label="预警类型" prop="warningType" min-width="120" />
-        <el-table-column label="预警编码" prop="warningCode" min-width="140" />
         <el-table-column label="最小值" prop="minValue" width="90" align="center" />
         <el-table-column label="最大值" prop="maxValue" width="90" align="center" />
         <el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
         <el-table-column label="创建时间" prop="createTime" width="170" />
         <el-table-column label="操作" width="160" align="center" fixed="right">
           <template #default="{ row }">
-            <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
-            <el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
+            <el-button v-if="row.id" type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
+            <span v-else class="muted">未配置</span>
+            <el-button v-if="row.id" type="danger" size="small" @click="handleDelete(row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -60,22 +51,11 @@
     <!-- 新增/编辑对话框 -->
     <el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px" destroy-on-close>
       <el-form :model="formData" :rules="rules" ref="formRef" label-width="100px">
-        <el-form-item label="选择设备" prop="deviceCode">
-          <el-select v-model="formData.deviceCode" placeholder="请选择设备" filterable clearable style="width: 100%"
-            @click="loadDrainageDeviceOptions">
-            <el-option v-for="d in deviceOptions" :key="d.equipmentCode"
-              :label="(d.equipmentCode || d.deviceCode) + ' - ' + (d.equipmentName || d.deviceName)"
-              :value="d.equipmentCode || d.deviceCode" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="预警类型" prop="warningType">
-          <el-select v-model="formData.warningType" placeholder="请选择预警类型" style="width: 100%" @change="onWarningTypeChange">
-            <el-option v-for="opt in WARNING_TYPE_OPTIONS" :key="opt.warningType" :label="opt.warningType" :value="opt.warningType" />
+        <el-form-item label="设备类型" prop="deviceType">
+          <el-select v-model="formData.deviceType" placeholder="请选择设备类型" filterable clearable style="width: 100%" @change="onDeviceTypeChange">
+            <el-option v-for="type in deviceTypeOptions" :key="type" :label="type" :value="type" />
           </el-select>
         </el-form-item>
-        <el-form-item label="预警编码">
-          <el-input v-model="formData.warningCode" placeholder="选择预警类型后自动带出" />
-        </el-form-item>
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="最小值" prop="minValue">
@@ -104,30 +84,17 @@
 import { ref, reactive, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import {
-  getThresholdModulePage, saveThreshold, updateThreshold, deleteThreshold,
-  getEquipmentPage
+  getThresholdModulePage, saveThreshold, updateThreshold, deleteThreshold
 } from '@/api/drainage'
-import request from '@/utils/request'
+import { getEquipmentByTopLevelType } from '@/api/pipeNetwork/basic'
 
-// ==================== 排水模块预警类型与预警编码参考 ====================
-const WARNING_TYPE_OPTIONS = [
-  { warningType: '水位预警', warningCode: 'WARN-WATER-LEVEL' },
-  { warningType: '流速预警', warningCode: 'WARN-FLOW-SPEED' },
-  { warningType: '悬浮物预警', warningCode: 'WARN-SUSPENDED-SOLIDS' },
-  { warningType: '氨氮预警', warningCode: 'WARN-AMMONIA-NITROGEN' },
-  { warningType: '电导率预警', warningCode: 'WARN-CONDUCTIVITY' },
-  { warningType: 'PH预警', warningCode: 'WARN-PH' },
-  { warningType: '瞬时流量预警', warningCode: 'WARN_INSTANT_FLOW' },
-  { warningType: '倾斜预警', warningCode: 'WARN-TILT' },
-  { warningType: '温度预警', warningCode: 'WARN-TEMPERATURE' },
-  { warningType: '湿度预警', warningCode: 'WARN-HUMIDITY' }
-]
+const DRAINAGE_MODULE_TYPE = '排水'
+const deviceTypeOptions = ref([])
+const filteredDeviceOptions = ref([])
 
 // ==================== 搜索参数 ====================
 const queryParams = reactive({
-  deviceCode: '',
-  warningType: '',
-  warningCode: ''
+  deviceCode: ''
 })
 const queryRef = ref(null)
 
@@ -137,8 +104,6 @@ function handleQuery() {
 }
 function resetQuery() {
   queryParams.deviceCode = ''
-  queryParams.warningType = ''
-  queryParams.warningCode = ''
   pageNum.value = 1
   loadData()
 }
@@ -155,9 +120,7 @@ async function loadData() {
   try {
     const params = {}
     if (queryParams.deviceCode) params.deviceCode = queryParams.deviceCode
-    if (queryParams.warningType) params.warningType = queryParams.warningType
-    if (queryParams.warningCode) params.warningCode = queryParams.warningCode
-    const res = await getThresholdModulePage(pageNum.value, pageSize.value, '排水', params)
+    const res = await getThresholdModulePage(pageNum.value, pageSize.value, DRAINAGE_MODULE_TYPE, params)
     const pageData = res.data || res
     tableData.value = pageData.records || []
     total.value = pageData.total || 0
@@ -178,41 +141,45 @@ const deviceInfoMap = ref({})
 async function ensureDeviceMap() {
   if (Object.keys(deviceInfoMap.value).length > 0) return
   try {
-    const res = await request({ url: '/EquipmentBase/findByTopLevelType', method: 'get', params: { typeName: '排水' } })
+    const res = await getEquipmentByTopLevelType(DRAINAGE_MODULE_TYPE)
     const list = res.data || []
     const map = {}
     list.forEach(d => { map[d.equipmentCode] = d })
     deviceInfoMap.value = map
+    deviceTypeOptions.value = [...new Set(list.map(d => d.equipmentTypeName).filter(Boolean))]
+    filteredDeviceOptions.value = list
   } catch (e) { console.error('加载排水设备列表失败', e) }
 }
 
 function getDeviceName(deviceCode) {
-  const d = deviceInfoMap.value[deviceCode]
-  return d ? d.equipmentName : deviceCode
+  return String(deviceCode || '').split(',').map(code => {
+    const d = deviceInfoMap.value[code.trim()]
+    return d ? (d.equipmentName || code.trim()) : code.trim()
+  }).filter(Boolean).join('、') || '-'
 }
 
 function getDeviceType(deviceCode) {
-  const d = deviceInfoMap.value[deviceCode]
-  return d ? (d.typeName || '-') : '-'
+  const types = String(deviceCode || '').split(',').map(code => deviceInfoMap.value[code.trim()])
+    .filter(Boolean).map(d => d.equipmentTypeName).filter(Boolean)
+  return [...new Set(types)].join('、') || '-'
 }
 
 // ==================== 新增/编辑 ====================
 const dialogVisible = ref(false)
-const dialogTitle = ref('新增阈值')
+const dialogTitle = ref('按设备类型批量修改阈值')
 const formRef = ref(null)
 const submitLoading = ref(false)
 const isEdit = ref(false)
 
 const rules = {
-  deviceCode: [{ required: true, message: '请选择设备', trigger: 'change' }],
-  warningType: [{ required: true, message: '请选择预警类型', trigger: 'change' }]
+  deviceType: [{ required: true, message: '请选择设备类型', trigger: 'change' }]
 }
 
 const formData = reactive({
   id: '',
+  deviceType: '',
+  deviceCodes: [],
   deviceCode: '',
-  warningType: '',
-  warningCode: '',
   minValue: null,
   maxValue: null,
   remark: ''
@@ -223,10 +190,10 @@ const deviceOptions = ref([])
 async function loadDrainageDeviceOptions() {
   if (deviceOptions.value.length > 0) return
   try {
-    // 优先使用排水设备专用接口
-    const res = await getEquipmentPage(1, 200, {})
-    const pageData = res.data || res
-    deviceOptions.value = pageData.records || pageData.rows || []
+    const res = await getEquipmentByTopLevelType(DRAINAGE_MODULE_TYPE)
+    deviceOptions.value = res.data || []
+    deviceTypeOptions.value = [...new Set(deviceOptions.value.map(d => d.equipmentTypeName).filter(Boolean))]
+    filteredDeviceOptions.value = deviceOptions.value
   } catch (e) {
     console.error('加载排水设备列表失败', e)
   }
@@ -234,20 +201,19 @@ async function loadDrainageDeviceOptions() {
 
 function resetForm() {
   formData.id = ''
+  formData.deviceType = ''
+  formData.deviceCodes = []
   formData.deviceCode = ''
-  formData.warningType = ''
-  formData.warningCode = ''
   formData.minValue = null
   formData.maxValue = null
   formData.remark = ''
   isEdit.value = false
-  dialogTitle.value = '新增阈值'
+  dialogTitle.value = '按设备类型批量修改阈值'
 }
 
-// 选择预警类型后自动带出预警编码
-function onWarningTypeChange(val) {
-  const opt = WARNING_TYPE_OPTIONS.find(o => o.warningType === val)
-  formData.warningCode = opt ? opt.warningCode : ''
+function onDeviceTypeChange(type) {
+  filteredDeviceOptions.value = deviceOptions.value.filter(d => d.equipmentTypeName === type)
+  formData.deviceCodes = filteredDeviceOptions.value.map(d => d.equipmentCode).filter(Boolean)
 }
 
 async function handleAdd() {
@@ -256,13 +222,16 @@ async function handleAdd() {
   dialogVisible.value = true
 }
 
-function handleEdit(row) {
+async function handleEdit(row) {
+  await loadDrainageDeviceOptions()
   isEdit.value = true
   dialogTitle.value = '编辑阈值'
   formData.id = row.id || ''
   formData.deviceCode = row.deviceCode
-  formData.warningType = row.warningType
-  formData.warningCode = row.warningCode || ''
+  formData.deviceCodes = String(row.deviceCode || '').split(',').map(v => v.trim()).filter(Boolean)
+  const firstDevice = deviceOptions.value.find(d => formData.deviceCodes.includes(d.equipmentCode))
+  formData.deviceType = firstDevice?.equipmentTypeName || ''
+  onDeviceTypeChange(formData.deviceType)
   formData.minValue = row.minValue
   formData.maxValue = row.maxValue
   formData.remark = row.remark || ''
@@ -272,23 +241,25 @@ function handleEdit(row) {
 async function handleSave() {
   const valid = await formRef.value.validate().catch(() => false)
   if (!valid) return
+  if (!formData.deviceCodes.length) {
+    ElMessage.warning('该设备类型下没有可配置的设备')
+    return
+  }
   submitLoading.value = true
   try {
     const data = {
-      deviceCode: formData.deviceCode,
-      warningType: formData.warningType,
-      warningCode: formData.warningCode || undefined,
+      deviceCode: formData.deviceCodes.join(','),
       minValue: formData.minValue,
       maxValue: formData.maxValue,
       remark: formData.remark || undefined
     }
     if (isEdit.value) {
       data.id = formData.id
-      await updateThreshold(data)
-      ElMessage.success('修改成功')
+      await updateThreshold(data, DRAINAGE_MODULE_TYPE)
+      ElMessage.success('设备类型阈值批量修改成功')
     } else {
-      await saveThreshold(data)
-      ElMessage.success('新增成功')
+      await saveThreshold(data, DRAINAGE_MODULE_TYPE)
+      ElMessage.success('设备类型阈值批量配置成功')
     }
     dialogVisible.value = false
     loadData()
@@ -337,4 +308,4 @@ onMounted(async () => {
 .table-card {
   min-height: 400px;
 }
-</style>
+</style>

+ 1 - 4
src/views/subSystem/drainage/jcyj/lsyjGIS.vue

@@ -146,10 +146,7 @@ const statusMap = { HANDLED: '已处置', CLOSED: '已解除', DRAFT: '草稿',
 async function loadData() {
   try {
     const res = await getWarningHistoryPage(1, 1000, {
-      equipmentType: 'drainage',
-      warningType: '2',
-      historyOnly: true,
-      status: 'HANDLED'
+      historyOnly: true
     })
     const pageData = res.code !== undefined ? res.data : res
     const records = pageData.records || []

+ 3 - 4
src/views/subSystem/drainage/jcyj/lsyjgl.vue

@@ -203,11 +203,10 @@ const filters = reactive({
 async function loadData() {
   loading.value = true
   try {
+    // 历史页由后端按历史状态(HANDLED/CLOSED)限定;不再写死
+    // 旧的数字类型和 HANDLED 状态,避免过滤掉实际历史记录。
     const params = {
-      equipmentType: 'drainage',
-      warningType: '2',
-      historyOnly: true,
-      status: 'HANDLED'
+      historyOnly: true
     }
     if (filters.equipmentName) params.equipmentName = filters.equipmentName
     if (filters.warningLevel) params.warningLevel = filters.warningLevel

+ 117 - 9
src/views/subSystem/gas/gasfxpg/gwfxpg.vue

@@ -162,6 +162,8 @@ import { MapLocation, Search, Refresh, FullScreen, Location, ZoomIn, ZoomOut } f
 import { ElMessage } from 'element-plus'
 import * as echarts from 'echarts'
 import { convertPipeline, convertPoints, convertCoord } from '@/utils/coordTransform'
+import { getGisPipeLines } from '@/api/pipeNetwork/basic'
+import { getGasRiskAssessments } from '@/api/pipeNetwork/hazard'
 
 // ==================== 百度地图初始化 ====================
 const MAP_CENTER = { lng: 110.386, lat: 28.445 }
@@ -201,16 +203,19 @@ function initMap() {
     currentLat.value = e.latlng.lat
   })
   renderAllOverlays()
+  // 风险接口和地图接口是并行加载的,若数据先返回,这里补一次按真实管线范围定位。
+  fitBounds()
 }
 
-// ==================== 沅陵县静态数据 ====================
+// ==================== 管网风险数据(由接口加载) ====================
 
 function riskLevelName(level) {
   return level === 'high' ? '高风险' : level === 'medium' ? '中风险' : '低风险'
 }
 
 // 管线风险数据(8段)
-const pipelines = ref([
+const pipelines = ref([])
+const demoPipelines = [
   {
     id: 1, code: 'PL-YL-001', name: '燃气1', area: '燃气1管线',
     riskLevel: 'high', riskScore: 86.5, riskLevelName: '高风险',
@@ -229,19 +234,21 @@ const pipelines = ref([
     riskCauses: ['沿线有在建工程', '防腐层老化', '人流量大'],
     trendData: [60.1, 61.0, 62.1, 63.2, 63.8, 64.3]
   },
-])
+]
 
 // 周边危险源
-const hazardPoints = ref([
+const hazardPoints = ref([])
+const demoHazardPoints = [
   { id: 'HZ1', name: '燃气1-D点危险源', type: '危险源', level: 'high', lng: 110.401084, lat: 28.433876, desc: '储配站', riskRadius: '500m' },
   { id: 'HZ2', name: '燃气2-B点危险源', type: '危险源', level: 'medium', lng: 110.372753, lat: 28.450901, desc: '计量调压站', riskRadius: '300m' },
-])
+]
 
-const targetPoints = ref([
+const targetPoints = ref([])
+const demoTargetPoints = [
   { id: 'TG1', name: '燃气1-E点防护目标', type: '防护目标', level: 'high', lng: 110.394299, lat: 28.434538, desc: '学校,约2000人' },
   { id: 'TG2', name: '燃气2-A点防护目标', type: '防护目标', level: 'medium', lng: 110.362363, lat: 28.453169, desc: '医院' },
   { id: 'TG3', name: '燃气1-F点防护目标', type: '防护目标', level: 'low', lng: 110.374231, lat: 28.43655, desc: '公园' },
-])
+]
 
 // 风险评分区间标记
 const riskMarks = { 0: '0', 25: '25', 50: '50', 75: '75', 100: '100' }
@@ -254,6 +261,12 @@ const quickLocation = ref('')
 
 const filteredPipelines = computed(() => {
   let list = pipelines.value
+  if (searchKeyword.value) {
+    const keyword = searchKeyword.value.toLowerCase()
+    list = list.filter(p => String(p.name || '').toLowerCase().includes(keyword)
+      || String(p.code || '').toLowerCase().includes(keyword)
+      || String(p.area || '').toLowerCase().includes(keyword))
+  }
   if (riskScoreRange.value) {
     list = list.filter(p => p.riskScore >= riskScoreRange.value[0] && p.riskScore <= riskScoreRange.value[1])
   }
@@ -264,7 +277,9 @@ const filteredPipelines = computed(() => {
 const highRiskCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'high').length)
 const mediumRiskCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'medium').length)
 const lowRiskCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'low').length)
-const avgRiskScore = computed(() => (pipelines.value.reduce((sum, p) => sum + p.riskScore, 0) / pipelines.value.length).toFixed(1))
+const avgRiskScore = computed(() => pipelines.value.length
+  ? (pipelines.value.reduce((sum, p) => sum + (Number(p.riskScore) || 0), 0) / pipelines.value.length).toFixed(1)
+  : '0.0')
 const hazardCount = computed(() => hazardPoints.value.length)
 const targetCount = computed(() => targetPoints.value.length)
 
@@ -275,6 +290,81 @@ const hoverY = ref(0)
 const currentPipeline = ref(null)
 const detailDrawerVisible = ref(false)
 
+function normalizeRiskPipeline(row, gisFeature) {
+  const properties = gisFeature?.properties || {}
+  const geometry = gisFeature?.geometry
+  let path = []
+  if (geometry?.type === 'LineString' && Array.isArray(geometry.coordinates)) {
+    path = geometry.coordinates
+  } else if (geometry?.type === 'MultiLineString' && Array.isArray(geometry.coordinates)) {
+    // 将多段管线展平成一条连续绘制路径,兼容 GIS 返回 MultiLineString。
+    path = geometry.coordinates.flat().filter(c => Array.isArray(c) && c.length >= 2)
+  }
+  if (path.length < 2 && Array.isArray(row.path)) path = row.path
+  if (path.length < 2 && Array.isArray(row.coordinates)) path = row.coordinates
+  // 部分后端版本将坐标放在 properties 中,统一兼容数字字符串。
+  const startLng = row.startLongitude ?? properties.startLongitude
+  const startLat = row.startLatitude ?? properties.startLatitude
+  const endLng = row.endLongitude ?? properties.endLongitude
+  const endLat = row.endLatitude ?? properties.endLatitude
+  if (path.length < 2 && startLng != null && startLat != null && endLng != null && endLat != null) {
+    path = [[Number(startLng), Number(startLat)], [Number(endLng), Number(endLat)]]
+  }
+  path = path
+    .map(c => [Number(c?.[0]), Number(c?.[1])])
+    .filter(c => Number.isFinite(c[0]) && Number.isFinite(c[1]))
+  return {
+    ...row,
+    id: row.id || row.networkId || properties.networkId,
+    code: row.code || row.networkCode || properties.networkCode || row.networkId,
+    name: row.name || row.networkName || properties.networkName || row.networkId,
+    area: row.area || row.location || '-',
+    riskLevel: row.riskLevel || 'low',
+    riskLevelName: row.riskLevelName || riskLevelName(row.riskLevel || 'low'),
+    riskScore: Number(row.riskScore) || 0,
+    path,
+    material: row.material || properties.material || '-',
+    pressureLevel: row.pressureLevel || row.networkLevel || '-',
+    length: Number(row.length) || 0,
+    riskCauses: Array.isArray(row.riskFactors) ? row.riskFactors : []
+  }
+}
+
+async function loadRiskData() {
+  try {
+    const [riskResponse, gisResponse] = await Promise.all([
+      getGasRiskAssessments(),
+      getGisPipeLines({ networkType: 'gas' })
+    ])
+    const gisData = gisResponse?.data || {}
+    const features = Array.isArray(gisData.features) ? gisData.features : []
+    const byKey = new Map()
+    features.forEach(feature => {
+      const p = feature.properties || {}
+      ;[
+        p.networkId, p.networkCode, p.id, p.code,
+        p.networkName, p.name
+      ].filter(Boolean).forEach(key => byKey.set(String(key).trim().toLowerCase(), feature))
+    })
+    const riskRows = Array.isArray(riskResponse?.data) ? riskResponse.data : []
+    pipelines.value = riskRows.map(row => {
+      const keys = [row.networkId, row.id, row.code, row.networkCode, row.name, row.networkName]
+        .filter(Boolean).map(key => String(key).trim().toLowerCase())
+      const feature = keys.map(key => byKey.get(key)).find(Boolean)
+      return normalizeRiskPipeline(row, feature)
+    })
+    currentPipeline.value = pipelines.value[0] || null
+    if (mapInstance) {
+      renderAllOverlays()
+      fitBounds()
+    }
+  } catch (e) {
+    pipelines.value = []
+    currentPipeline.value = null
+    ElMessage.error('燃气管网风险数据加载失败')
+  }
+}
+
 // ==================== 地图覆盖物渲染 ====================
 function clearOverlays() {
   Object.values(overlayStore).forEach(arr => {
@@ -290,6 +380,7 @@ function addToStore(layer, overlay) {
 function renderPipelines() {
   const BMapGL = window.BMapGL
   filteredPipelines.value.forEach(p => {
+    if (!Array.isArray(p.path) || p.path.length < 2) return
     const layerKey = p.riskLevel
     const points = p.path.map(c => { const [lng, lat] = convertCoord(c[0], c[1]); return new BMapGL.Point(lng, lat) })
     const color = p.riskLevel === 'high' ? '#f56c6c' : p.riskLevel === 'medium' ? '#e6a23c' : '#67c23a'
@@ -444,13 +535,29 @@ function zoomOut() {
 function fitBounds() {
   if (mapInstance) {
     const BMapGL = window.BMapGL
+    const coordinates = filteredPipelines.value
+      .flatMap(p => Array.isArray(p.path) ? p.path : [])
+      .filter(c => Number.isFinite(Number(c?.[0])) && Number.isFinite(Number(c?.[1])))
+    if (coordinates.length >= 2) {
+      const points = coordinates.map(c => {
+        const [lng, lat] = convertCoord(Number(c[0]), Number(c[1]))
+        return new BMapGL.Point(lng, lat)
+      })
+      try {
+        mapInstance.setViewport(points, { margins: [40, 40, 40, 40], zoomFactor: -1 })
+      } catch {
+        // 兼容旧版百度地图 GL 不支持 viewportOptions 的情况。
+        mapInstance.setViewport(points)
+      }
+      return
+    }
     const [clng, clat] = convertCoord(MAP_CENTER.lng, MAP_CENTER.lat)
     mapInstance.centerAndZoom(new BMapGL.Point(clng, clat), MAP_DEFAULT_ZOOM)
   }
 }
 
 function refreshMap() {
-  ElMessage.success('地图已刷新')
+  loadRiskData().then(() => ElMessage.success('地图已刷新'))
 }
 
 function locateArea() {
@@ -562,6 +669,7 @@ watch(detailDrawerVisible, (val) => {
 
 onMounted(async () => {
   window.addEventListener('mousemove', onWindowMouseMove)
+  loadRiskData()
   try {
     await waitForBMapGL()
     nextTick(() => initMap())

+ 26 - 125
src/views/subSystem/gas/gasfxpg/gwfxpgqd.vue

@@ -228,149 +228,36 @@
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
+import { ref, computed, onMounted } from 'vue'
 import { DataAnalysis, Search, Refresh, Download, Location, Document, Upload } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
+import { getGasRiskAssessments } from '@/api/pipeNetwork/hazard'
 
-// 沅陵县管网风险评估数据(8段)
-const pipelines = ref([
-  {
-    id: 1, code: 'PL-YL-001', name: '辰州路高压管线', area: '沅陵县太常片区', material: '钢管', diameter: 500, length: 3.8,
-    buildYear: '2015', pressure: 1.6, owner: '沅陵燃气有限公司', lastInspection: '2026-03-15',
-    riskLevel: 'high', riskScore: 86.5,
-    dimensions: { leakProbability: 85, consequenceSeverity: 92, corrosionRisk: 78, thirdPartyRisk: 88, agingLevel: 82 },
-    riskFactors: ['管道服役超过10年,老化程度较高', '辰州路主干道交通密集,第三方破坏风险大', '防腐层检测发现多处局部破损', '沿线有市政雨污分流施工活动'],
-    mitigationMeasures: ['立即安排管道内检测及完整性评价', '加密巡检频次至每周2次', '在施工区域设置警示标识并派专人监护', '制定辰州路段专项应急预案'],
-    historyRecords: [
-      { id: 1, time: '2026-03-15', riskLevel: 'high', score: 86.5, type: 'danger', description: '2026年度春季风险评估,高风险,腐蚀加剧' },
-      { id: 2, time: '2025-09-20', riskLevel: 'high', score: 84.2, type: 'danger', description: '秋季评估,因施工活动风险上调' },
-      { id: 3, time: '2025-03-10', riskLevel: 'medium', score: 68.5, type: 'warning', description: '春季评估,首次发现防腐层破损' },
-      { id: 4, time: '2024-09-05', riskLevel: 'medium', score: 62.3, type: 'primary', description: '首次全面风险评估' }
-    ],
-    attachments: [{ name: '辰州路高压管线检测报告_20260315.pdf' }, { name: '管线竣工图纸.dwg' }, { name: '防腐层检测记录.xlsx' }]
-  },
-  {
-    id: 2, code: 'PL-YL-002', name: '建设东路高压管', area: '沅陵县太常片区', material: '钢管', diameter: 400, length: 3.6,
-    buildYear: '2016', pressure: 1.6, owner: '沅陵燃气有限公司', lastInspection: '2025-11-08',
-    riskLevel: 'high', riskScore: 79.2,
-    dimensions: { leakProbability: 82, consequenceSeverity: 76, corrosionRisk: 85, thirdPartyRisk: 72, agingLevel: 88 },
-    riskFactors: ['高压运行风险等级高', '管道穿越工业园区域,周边有化工企业', '检测发现多处腐蚀点,局部壁厚减薄', '建设路沿线重型车辆通行频繁'],
-    mitigationMeasures: ['紧急申请维修资金进行局部换管', '加强对化工企业周边管段的气体监测', '在工业园段增设阴极保护', '每季度进行一次壁厚检测'],
-    historyRecords: [
-      { id: 1, time: '2025-11-08', riskLevel: 'high', score: 79.2, type: 'danger', description: '检测发现严重腐蚀,壁厚减薄超限' },
-      { id: 2, time: '2025-06-15', riskLevel: 'high', score: 76.8, type: 'danger', description: '中期评估,腐蚀风险升高' },
-      { id: 3, time: '2025-01-20', riskLevel: 'medium', score: 61.5, type: 'warning', description: '年度风险评估,发现局部异常' }
-    ],
-    attachments: [{ name: '建设东路高压管腐蚀检测报告.pdf' }, { name: '壁厚检测数据.xlsx' }, { name: '维修方案_v2.docx' }]
-  },
-  {
-    id: 3, code: 'PL-YL-003', name: '古城路中压管线', area: '沅陵县太常片区', material: '钢管', diameter: 300, length: 3.2,
-    buildYear: '2018', pressure: 0.4, owner: '沅陵燃气有限公司', lastInspection: '2026-02-10',
-    riskLevel: 'medium', riskScore: 64.3,
-    dimensions: { leakProbability: 58, consequenceSeverity: 72, corrosionRisk: 55, thirdPartyRisk: 68, agingLevel: 42 },
-    riskFactors: ['沿线有在建房地产开发工程', '部分区域防腐层老化脱落', '古城路商业区人流量大,后果严重性较高', '管线穿越古城地下管沟区域,交叉风险'],
-    mitigationMeasures: ['对施工区域加密巡查至每周3次', '计划2026年Q3进行内检测', '与开发商签订管线保护协议', '更新防腐层老化段'],
-    historyRecords: [
-      { id: 1, time: '2026-02-10', riskLevel: 'medium', score: 64.3, type: 'warning', description: '季度评估,施工活动导致风险上升' },
-      { id: 2, time: '2025-11-15', riskLevel: 'medium', score: 61.8, type: 'primary', description: '秋季风险评估,防腐层检测' },
-      { id: 3, time: '2025-08-20', riskLevel: 'medium', score: 58.5, type: 'primary', description: '夏季常规评估' },
-      { id: 4, time: '2025-05-10', riskLevel: 'low', score: 38.2, type: 'success', description: '春季评估,风险较低' }
-    ],
-    attachments: [{ name: '古城路中压管线检测报告_20260210.pdf' }, { name: '管沟交叉段详图.dwg' }, { name: '巡检记录_2026Q1.xlsx' }]
-  },
-  {
-    id: 4, code: 'PL-YL-004', name: '迎宾路中压管线', area: '沅陵县太常片区', material: '钢管', diameter: 300, length: 3.6,
-    buildYear: '2019', pressure: 0.4, owner: '沅陵燃气有限公司', lastInspection: '2026-04-28',
-    riskLevel: 'medium', riskScore: 55.8,
-    dimensions: { leakProbability: 48, consequenceSeverity: 62, corrosionRisk: 52, thirdPartyRisk: 58, agingLevel: 38 },
-    riskFactors: ['管道运行正常但有轻微腐蚀迹象', '迎宾路商业区人流较大', '沿线有电力管沟交叉,存在杂散电流干扰'],
-    mitigationMeasures: ['加强日常巡检,每月至少2次', '排查电力管沟交叉段杂散电流影响', '计划2026年Q4进行防腐层检测'],
-    historyRecords: [
-      { id: 1, time: '2026-04-28', riskLevel: 'medium', score: 55.8, type: 'warning', description: '春季评估,轻微腐蚀需关注' },
-      { id: 2, time: '2025-10-15', riskLevel: 'medium', score: 52.3, type: 'primary', description: '秋季常规评估' },
-      { id: 3, time: '2025-04-20', riskLevel: 'medium', score: 50.1, type: 'primary', description: '年度风险评估' }
-    ],
-    attachments: [{ name: '迎宾路中压管线运行记录.pdf' }, { name: '杂散电流检测报告.docx' }]
-  },
-  {
-    id: 5, code: 'PL-YL-005', name: '天宁路中压管线', area: '沅陵县太常片区', material: 'PE管', diameter: 250, length: 3.8,
-    buildYear: '2020', pressure: 0.4, owner: '沅陵燃气有限公司', lastInspection: '2026-05-12',
-    riskLevel: 'medium', riskScore: 48.2,
-    dimensions: { leakProbability: 42, consequenceSeverity: 55, corrosionRisk: 28, thirdPartyRisk: 65, agingLevel: 25 },
-    riskFactors: ['PE管接头处需定期检测维护', '天宁西路有地下车库临近施工', '管道穿越太常安置区,人口密集', '地质沉降风险需关注'],
-    mitigationMeasures: ['每年对PE管接头进行超声波检测', '临近施工期间加密巡查', '建立安置区居民燃气安全宣传机制'],
-    historyRecords: [
-      { id: 1, time: '2026-05-12', riskLevel: 'medium', score: 48.2, type: 'warning', description: '夏季评估,地下车库施工影响' },
-      { id: 2, time: '2025-11-20', riskLevel: 'low', score: 35.6, type: 'success', description: '秋季评估,风险可控' },
-      { id: 3, time: '2025-05-10', riskLevel: 'low', score: 32.8, type: 'success', description: '春季常规评估' }
-    ],
-    attachments: [{ name: '天宁路中压PE管检测报告.pdf' }, { name: '接头检测记录_2026.xlsx' }]
-  },
-  {
-    id: 6, code: 'PL-YL-006', name: '辰州北路中压管线', area: '沅陵县太常片区', material: 'PE管', diameter: 200, length: 1.2,
-    buildYear: '2021', pressure: 0.4, owner: '沅陵燃气有限公司', lastInspection: '2026-05-28',
-    riskLevel: 'medium', riskScore: 42.5,
-    dimensions: { leakProbability: 38, consequenceSeverity: 48, corrosionRisk: 20, thirdPartyRisk: 55, agingLevel: 18 },
-    riskFactors: ['辰州北路综合管廊相邻区域,交叉风险', '管段较短但周边地下管线复杂', '电力、通信管线共用管廊通道'],
-    mitigationMeasures: ['加强管廊交叉段气体监测', '与管廊管理单位建立联动机制', '定期检查PE管与管廊的隔离情况'],
-    historyRecords: [
-      { id: 1, time: '2026-05-28', riskLevel: 'medium', score: 42.5, type: 'warning', description: '管廊交叉段专项评估' },
-      { id: 2, time: '2025-11-10', riskLevel: 'low', score: 35.2, type: 'success', description: '秋季评估,低风险' },
-      { id: 3, time: '2025-05-05', riskLevel: 'low', score: 30.8, type: 'success', description: '春季常规评估' }
-    ],
-    attachments: [{ name: '辰州北路管廊交叉段评估报告.pdf' }, { name: '管廊通道图纸.dwg' }]
-  },
-  {
-    id: 7, code: 'PL-YL-007', name: '滨江路低压管线', area: '沅陵县太常片区', material: 'PE管', diameter: 200, length: 3.8,
-    buildYear: '2022', pressure: 0.1, owner: '沅陵燃气有限公司', lastInspection: '2026-06-03',
-    riskLevel: 'low', riskScore: 28.6,
-    dimensions: { leakProbability: 25, consequenceSeverity: 35, corrosionRisk: 18, thirdPartyRisk: 32, agingLevel: 15 },
-    riskFactors: ['新建管道状况良好,整体风险低', '低压运行,泄漏后果可控', '滨江路河堤段需关注汛期水土流失影响'],
-    mitigationMeasures: ['常规巡检(每月1次)', '汛期前后加强河堤段管线上方土体检查', '建立运行档案,记录运行参数'],
-    historyRecords: [
-      { id: 1, time: '2026-06-03', riskLevel: 'low', score: 28.6, type: 'success', description: '夏季汛期前风险评估,低风险' },
-      { id: 2, time: '2025-12-10', riskLevel: 'low', score: 26.2, type: 'success', description: '冬季评估,运行正常' },
-      { id: 3, time: '2025-06-05', riskLevel: 'low', score: 24.5, type: 'success', description: '首次年度风险评估' }
-    ],
-    attachments: [{ name: '滨江路低压管线竣工图纸.pdf' }, { name: '运行记录_2026.xlsx' }]
-  },
-  {
-    id: 8, code: 'PL-YL-008', name: '太常南路低压管线', area: '沅陵县太常片区', material: 'PE管', diameter: 160, length: 1.5,
-    buildYear: '2023', pressure: 0.1, owner: '沅陵燃气有限公司', lastInspection: '2026-06-08',
-    riskLevel: 'low', riskScore: 18.3,
-    dimensions: { leakProbability: 18, consequenceSeverity: 22, corrosionRisk: 10, thirdPartyRisk: 25, agingLevel: 8 },
-    riskFactors: ['新建管道风险极低', '位于城南新开发区域,周边环境稳定', '管径小、压力低,风险可控'],
-    mitigationMeasures: ['常规巡检(每2月1次)', '关注区域开发建设动态', '定期更新电子档案'],
-    historyRecords: [
-      { id: 1, time: '2026-06-08', riskLevel: 'low', score: 18.3, type: 'success', description: '年度风险评估,新建管道运行良好' },
-      { id: 2, time: '2025-12-15', riskLevel: 'low', score: 16.8, type: 'success', description: '首次运行评估,各项指标正常' }
-    ],
-    attachments: [{ name: '太常南路竣工图纸.pdf' }, { name: '管道材质证明.pdf' }, { name: '验收报告.pdf' }]
-  }
-])
-
+// 页面数据来自后端初版风险模型(管网基础信息+近30天报警);保留字段结构以兼容详情展示。
+const pipelines = ref([])
 // 统计数据
 const totalPipelines = computed(() => pipelines.value.length)
 const highRiskCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'high').length)
 const mediumRiskCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'medium').length)
 const lowRiskCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'low').length)
-const highRiskRate = computed(() => ((highRiskCount.value / totalPipelines.value) * 100).toFixed(1))
-const mediumRiskRate = computed(() => ((mediumRiskCount.value / totalPipelines.value) * 100).toFixed(1))
-const lowRiskRate = computed(() => ((lowRiskCount.value / totalPipelines.value) * 100).toFixed(1))
+const riskRate = count => totalPipelines.value ? ((count / totalPipelines.value) * 100).toFixed(1) : '0.0'
+const highRiskRate = computed(() => riskRate(highRiskCount.value))
+const mediumRiskRate = computed(() => riskRate(mediumRiskCount.value))
+const lowRiskRate = computed(() => riskRate(lowRiskCount.value))
 
 // 筛选
 const searchKeyword = ref('')
 const riskFilter = ref('')
 const currentPage = ref(1)
 const pageSize = 5
-const selectedPipeline = ref(pipelines.value[0])
+const selectedPipeline = ref(null)
 const activeTab = ref('basic')
 
 const filteredPipelines = computed(() => {
   let list = pipelines.value
   if (searchKeyword.value) {
     const kw = searchKeyword.value.toLowerCase()
-    list = list.filter(p => p.name.toLowerCase().includes(kw) || p.code.toLowerCase().includes(kw) || p.area.toLowerCase().includes(kw))
+    list = list.filter(p => [p.name, p.code, p.area].some(value => String(value || '').toLowerCase().includes(kw)))
   }
   if (riskFilter.value) {
     list = list.filter(p => p.riskLevel === riskFilter.value)
@@ -404,9 +291,23 @@ const getProgressColor = (percentage) => {
 }
 
 const selectPipeline = (pipeline) => { selectedPipeline.value = pipeline }
-const refreshData = () => { ElMessage.success('数据已刷新') }
+async function loadRiskData() {
+  try {
+    const res = await getGasRiskAssessments()
+    const rows = Array.isArray(res.data) ? res.data : []
+    pipelines.value = rows
+    selectedPipeline.value = rows[0] || null
+  } catch (e) {
+    pipelines.value = []
+    selectedPipeline.value = null
+    ElMessage.error('风险评估数据加载失败')
+  }
+}
+const refreshData = () => { loadRiskData() }
 const exportReport = () => { ElMessage.success('导出风险评估报表(演示)') }
 const editArchive = () => { ElMessage.info('编辑电子档案(演示)') }
+
+onMounted(loadRiskData)
 </script>
 
 <style scoped>
@@ -558,4 +459,4 @@ li { margin: 6px 0; font-size: 13px; }
 .upload-text { font-size: 12px; color: #909399; }
 
 .pagination-area { padding: 12px; text-align: center; border-top: 1px solid #eee; }
-</style>
+</style>

+ 56 - 10
src/views/subSystem/gas/gasfxpg/gwfxxq.vue

@@ -185,9 +185,11 @@ import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
 import { DataAnalysis, Search, Refresh, Download, Location, Monitor, Document, Warning, OfficeBuilding, CircleClose } from '@element-plus/icons-vue'
 import * as echarts from 'echarts'
 import { ElMessage } from 'element-plus'
+import { getGasRiskAssessments } from '@/api/pipeNetwork/hazard'
 
-// 沅陵县管网风险构成分析数据(8段)
-const pipelines = ref([
+// 接口返回的燃气管网风险数据;保留旧示例数据结构仅用于字段兼容,运行时不再展示静态数据。
+const pipelines = ref([])
+const demoPipelines = [
   {
     id: 1, code: 'PL-YL-001', name: '辰州路高压管线', area: '沅陵县太常片区辰州路',
     riskLevel: 'high', riskScore: 86.5,
@@ -385,10 +387,10 @@ const pipelines = ref([
     ],
     radarData: [8, 18, 10, 12, 22]
   }
-])
+]
 
 // 统计数据
-const totalLength = computed(() => pipelines.value.reduce((sum, p) => sum + p.length, 0).toFixed(1))
+const totalLength = computed(() => pipelines.value.reduce((sum, p) => sum + (Number(p.length) || 0), 0).toFixed(1))
 const totalPipelines = computed(() => pipelines.value.length)
 const highRiskCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'high').length)
 
@@ -397,7 +399,7 @@ const searchKeyword = ref('')
 const riskFilter = ref('')
 const currentPage = ref(1)
 const pageSize = 5
-const selectedPipeline = ref(pipelines.value[0])
+const selectedPipeline = ref(null)
 
 const filteredPipelines = computed(() => {
   let list = pipelines.value
@@ -463,12 +465,56 @@ const selectPipeline = (pipeline) => {
   nextTick(() => renderRadarChart())
 }
 
-const refreshData = () => { ElMessage.success('数据已刷新') }
+function normalizeRiskPipeline(row) {
+  const dimensions = row.dimensions || {}
+  return {
+    ...row,
+    id: row.id || row.networkId,
+    code: row.code || row.networkCode || row.networkId,
+    name: row.name || row.networkName || row.networkId,
+    area: row.area || row.location || '-',
+    riskLevel: row.riskLevel || 'low',
+    riskScore: Number(row.riskScore) || 0,
+    material: row.material || '-',
+    pressureLevel: row.pressureLevel || row.networkLevel || '-',
+    diameter: row.diameter,
+    length: Number(row.length) || 0,
+    buryYear: row.buildYear ? `${row.buildYear}年` : '-',
+    designLife: row.designLife || '-',
+    owner: row.owner || '-',
+    maintenanceRecords: Array.isArray(row.maintenanceRecords) ? row.maintenanceRecords : [],
+    hazards: Array.isArray(row.hazards) ? row.hazards : [],
+    protectionTargets: Array.isArray(row.protectionTargets) ? row.protectionTargets : [],
+    riskCauses: Array.isArray(row.riskFactors) ? row.riskFactors : [],
+    radarData: [
+      Number(dimensions.agingLevel) || 0,
+      Number(dimensions.thirdPartyRisk) || 0,
+      Number(dimensions.corrosionRisk) || 0,
+      Number(dimensions.consequenceSeverity) || 0,
+      Number(dimensions.leakProbability) || 0
+    ]
+  }
+}
+
+async function loadRiskData() {
+  try {
+    const res = await getGasRiskAssessments()
+    const rows = Array.isArray(res.data) ? res.data.map(normalizeRiskPipeline) : []
+    pipelines.value = rows
+    selectedPipeline.value = rows[0] || null
+    currentPage.value = 1
+    nextTick(() => renderRadarChart())
+  } catch (e) {
+    pipelines.value = []
+    selectedPipeline.value = null
+    ElMessage.error('风险评估数据加载失败')
+  }
+}
+
+const refreshData = () => { loadRiskData() }
 const exportReport = () => { ElMessage.success('导出风险分析报告(演示)') }
 
-onMounted(() => {
-  nextTick(() => renderRadarChart())
-})
+onMounted(loadRiskData)
 
 onBeforeUnmount(() => {
   if (radarInstance) {
@@ -602,4 +648,4 @@ onBeforeUnmount(() => {
 .cause-item .el-icon { color: #f56c6c; }
 
 .pagination-area { padding: 12px; text-align: center; border-top: 1px solid #eee; }
-</style>
+</style>

+ 92 - 26
src/views/subSystem/gas/gasfxpg/gxfxpgsst.vue

@@ -172,6 +172,8 @@ import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
 import { MapLocation, Search, Refresh, FullScreen, Location, ZoomIn, ZoomOut, InfoFilled } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 import { convertCoord } from '@/utils/coordTransform'
+import { getGisPipeLines } from '@/api/pipeNetwork/basic'
+import { getGasRiskAssessments } from '@/api/pipeNetwork/hazard'
 
 // ==================== 百度地图初始化 ====================
 const MAP_CENTER = { lng: 110.386, lat: 28.445 }
@@ -211,6 +213,7 @@ function initMap() {
     currentLat.value = e.latlng.lat
   })
   renderAllOverlays()
+  fitBounds()
 }
 
 // ==================== 沅陵县燃气管网四色风险数据 ====================
@@ -231,24 +234,76 @@ function getRiskColor(level) {
   return map[level]
 }
 
-const pipelines = ref([
-  {
-    id: 1, code: 'PL-YL-001', name: '燃气1',
-    riskScore: 86.5, riskLevel: 'major', riskLevelName: '重大风险', color: '#f56c6c', lineWidth: 6,
-    material: '钢管', pressureLevel: '高压 (1.6MPa)', diameter: 500, length: 3.8,
-    buryYear: '2015年', owner: '燃气公司', lastInspection: '2026-03-15',
-    path: [[110.404947, 28.433419], [110.404836, 28.433429], [110.403275, 28.433571], [110.401084, 28.433876], [110.399601, 28.433876], [110.398112, 28.433979], [110.396717, 28.434314], [110.394299, 28.434538], [110.394352, 28.434887], [110.391969, 28.434985], [110.384374, 28.435695], [110.381418, 28.43559], [110.377993, 28.436193], [110.376062, 28.436385], [110.374231, 28.43655], [110.372584, 28.437107], [110.371292, 28.4377], [110.370581, 28.438198], [110.369831, 28.438821]],
-    riskFactors: ['管道服役超过10年', '交通密集', '防腐层多处破损', '沿线施工活动']
-  },
-  {
-    id: 2, code: 'PL-YL-002', name: '燃气2',
-    riskScore: 64.3, riskLevel: 'larger', riskLevelName: '较大风险', color: '#e6a23c', lineWidth: 5,
-    material: 'PE管', pressureLevel: '中压 (0.4MPa)', diameter: 300, length: 3.2,
-    buryYear: '2018年', owner: '燃气公司', lastInspection: '2026-02-10',
-    path: [[110.361433, 28.454979], [110.362363, 28.453169], [110.362851, 28.452213], [110.364377, 28.452443], [110.365138, 28.45246], [110.366462, 28.452157], [110.369549, 28.451441], [110.37219, 28.450897], [110.372753, 28.450901], [110.373875, 28.451194], [110.373913, 28.451107], [110.374597, 28.451476]],
-    riskFactors: ['沿线有在建工程', '防腐层老化', '人流量大']
-  },
-])
+// 风险评估四色图使用真实风险评估及 GIS 管线接口,不保留静态示例数据。
+const pipelines = ref([])
+
+function normalizePipeline(row, feature) {
+  const properties = feature?.properties || {}
+  const geometry = feature?.geometry
+  let path = geometry?.type === 'LineString' && Array.isArray(geometry.coordinates)
+    ? geometry.coordinates : []
+  if (geometry?.type === 'MultiLineString' && Array.isArray(geometry.coordinates)) {
+    path = geometry.coordinates.flat().filter(c => Array.isArray(c) && c.length >= 2)
+  }
+  const startLng = row.startLongitude ?? properties.startLongitude
+  const startLat = row.startLatitude ?? properties.startLatitude
+  const endLng = row.endLongitude ?? properties.endLongitude
+  const endLat = row.endLatitude ?? properties.endLatitude
+  if (path.length < 2 && [startLng, startLat, endLng, endLat].every(v => v != null)) {
+    path = [[Number(startLng), Number(startLat)], [Number(endLng), Number(endLat)]]
+  }
+  path = (Array.isArray(row.path) && path.length < 2 ? row.path : path)
+    .map(c => [Number(c?.[0]), Number(c?.[1])])
+    .filter(c => Number.isFinite(c[0]) && Number.isFinite(c[1]))
+  const riskScore = Number(row.riskScore) || 0
+  const riskLevel = getRiskLevel(riskScore)
+  return {
+    ...row,
+    id: row.id || row.networkId || properties.networkId,
+    code: row.code || row.networkCode || properties.networkCode || row.networkId || '',
+    name: row.name || row.networkName || properties.networkName || row.networkId || '未命名管段',
+    riskScore,
+    riskLevel,
+    riskLevelName: getRiskLevelName(riskLevel),
+    color: getRiskColor(riskLevel),
+    lineWidth: riskLevel === 'major' ? 6 : riskLevel === 'larger' ? 5 : riskLevel === 'general' ? 4 : 3,
+    material: row.material || properties.material || '-',
+    pressureLevel: row.pressureLevel || row.networkLevel || '-',
+    diameter: Number(row.diameter) || 0,
+    length: Number(row.length) || 0,
+    path,
+    riskFactors: Array.isArray(row.riskFactors) ? row.riskFactors : []
+  }
+}
+
+async function loadRiskData() {
+  try {
+    const [riskResponse, gisResponse] = await Promise.all([
+      getGasRiskAssessments(),
+      getGisPipeLines({ networkType: 'gas' })
+    ])
+    const features = Array.isArray(gisResponse?.data?.features) ? gisResponse.data.features : []
+    const byKey = new Map()
+    features.forEach(feature => {
+      const p = feature.properties || {}
+      ;[p.networkId, p.networkCode, p.id, p.code, p.networkName, p.name]
+        .filter(Boolean).forEach(key => byKey.set(String(key).trim().toLowerCase(), feature))
+    })
+    const rows = Array.isArray(riskResponse?.data) ? riskResponse.data : []
+    pipelines.value = rows.map(row => {
+      const keys = [row.networkId, row.id, row.code, row.networkCode, row.name, row.networkName]
+        .filter(Boolean).map(key => String(key).trim().toLowerCase())
+      return normalizePipeline(row, keys.map(key => byKey.get(key)).find(Boolean))
+    })
+    if (mapInstance) {
+      renderAllOverlays()
+      fitBounds()
+    }
+  } catch (e) {
+    pipelines.value = []
+    ElMessage.error('燃气管线风险数据加载失败')
+  }
+}
 
 // ==================== 统计数据 ====================
 const majorCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'major').length)
@@ -256,13 +311,13 @@ const largerCount = computed(() => pipelines.value.filter(p => p.riskLevel === '
 const generalCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'general').length)
 const lowCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'low').length)
 const totalCount = computed(() => pipelines.value.length)
-const majorPercent = computed(() => ((majorCount.value / totalCount.value) * 100).toFixed(1))
-const largerPercent = computed(() => ((largerCount.value / totalCount.value) * 100).toFixed(1))
-const generalPercent = computed(() => ((generalCount.value / totalCount.value) * 100).toFixed(1))
-const lowPercent = computed(() => ((lowCount.value / totalCount.value) * 100).toFixed(1))
+const majorPercent = computed(() => totalCount.value ? ((majorCount.value / totalCount.value) * 100).toFixed(1) : '0.0')
+const largerPercent = computed(() => totalCount.value ? ((largerCount.value / totalCount.value) * 100).toFixed(1) : '0.0')
+const generalPercent = computed(() => totalCount.value ? ((generalCount.value / totalCount.value) * 100).toFixed(1) : '0.0')
+const lowPercent = computed(() => totalCount.value ? ((lowCount.value / totalCount.value) * 100).toFixed(1) : '0.0')
 const riskIndex = computed(() => {
   const totalScore = pipelines.value.reduce((sum, p) => sum + p.riskScore, 0)
-  return (totalScore / totalCount.value).toFixed(1)
+  return totalCount.value ? (totalScore / totalCount.value).toFixed(1) : '0.0'
 })
 const riskIndexClass = computed(() => {
   const score = parseFloat(riskIndex.value)
@@ -279,7 +334,7 @@ const filteredPipelines = computed(() => {
   let list = pipelines.value
   if (searchKeyword.value) {
     const kw = searchKeyword.value.toLowerCase()
-    list = list.filter(p => p.name.toLowerCase().includes(kw) || p.code.toLowerCase().includes(kw))
+    list = list.filter(p => String(p.name || '').toLowerCase().includes(kw) || String(p.code || '').toLowerCase().includes(kw))
   }
   return list.filter(p => visibleRisks.value.includes(p.riskLevel))
 })
@@ -307,6 +362,7 @@ function renderPipelines() {
   const BMapGL = window.BMapGL
   pipelines.value.forEach(p => {
     if (!visibleRisks.value.includes(p.riskLevel)) return
+    if (!Array.isArray(p.path) || p.path.length < 2) return
     const points = p.path.map(c => { const [lng, lat] = convertCoord(c[0], c[1]); return new BMapGL.Point(lng, lat) })
     const polyline = new BMapGL.Polyline(points, {
       strokeColor: p.color, strokeWeight: p.lineWidth, strokeOpacity: 0.82
@@ -376,14 +432,23 @@ function zoomOut() {
 function fitBounds() {
   if (mapInstance) {
     const BMapGL = window.BMapGL
+    const coordinates = filteredPipelines.value.flatMap(p => Array.isArray(p.path) ? p.path : [])
+    if (coordinates.length >= 2) {
+      const points = coordinates.map(c => {
+        const [lng, lat] = convertCoord(Number(c[0]), Number(c[1]))
+        return new BMapGL.Point(lng, lat)
+      })
+      try { mapInstance.setViewport(points, { margins: [40, 40, 40, 40], zoomFactor: -1 }) }
+      catch { mapInstance.setViewport(points) }
+      return
+    }
     const [clng, clat] = convertCoord(MAP_CENTER.lng, MAP_CENTER.lat)
     mapInstance.centerAndZoom(new BMapGL.Point(clng, clat), MAP_DEFAULT_ZOOM)
   }
 }
 
 function refreshMap() {
-  renderAllOverlays()
-  ElMessage.success('地图已刷新')
+  loadRiskData().then(() => ElMessage.success('地图已刷新'))
 }
 
 function locateSearch() {
@@ -428,6 +493,7 @@ function onWindowMouseMove(e) {
 // ==================== 生命周期 ====================
 onMounted(async () => {
   window.addEventListener('mousemove', onWindowMouseMove)
+  loadRiskData()
   try {
     await waitForBMapGL()
     nextTick(() => initMap())

+ 3 - 3
src/views/subSystem/gas/ghome/gHome.vue

@@ -388,9 +388,9 @@ onBeforeUnmount(() => {
               <tbody>
                 <tr v-for="row in pointRows" :key="row.id">
                   <td>{{ row.id }}</td>
-                  <td>{{ row.name }}</td>
-                  <td>{{ row.time }}</td>
-                  <td>{{ row.value }}</td>
+                  <td :title="`${row.name || '-'} / ${row.deviceName || row.deviceCode || '-'}`">{{ row.name }} / {{ row.deviceName || row.deviceCode || '-' }}</td>
+                  <td :title="row.time">{{ row.time }}</td>
+                  <td :title="row.value">{{ row.value }}</td>
                   <td :class="['status-cell', row.status === '在线' ? 'ok' : row.status === '预警' ? 'warn' : 'off']">{{ row.status }}</td>
                 </tr>
               </tbody>