2 次代碼提交 60f241eaf3 ... 9a4f22de60

作者 SHA1 備註 提交日期
  null 9a4f22de60 Merge remote-tracking branch 'origin/master' 1 月之前
  null 1583aa9c37 feat: 管点/窨井设备关联功能及多项UI优化 1 月之前

+ 2 - 1
package.json

@@ -20,12 +20,13 @@
     "@vueup/vue-quill": "1.2.0",
     "@vueuse/core": "10.11.0",
     "axios": "0.28.1",
-    "echarts": "5.5.1",
+    "echarts": "^5.6.0",
     "element-plus": "2.7.6",
     "file-saver": "2.0.5",
     "fuse.js": "6.6.2",
     "js-cookie": "3.0.5",
     "jsencrypt": "3.3.2",
+    "leaflet": "^1.9.4",
     "nprogress": "0.2.0",
     "pinia": "2.1.7",
     "vue": "3.4.31",

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

@@ -43,6 +43,20 @@ export function deleteManhole(id) {
   return request({ url: '/api/manhole/deleteById', method: 'delete', params: { id } })
 }
 
+// ============ 窨井-设备关联 ============
+export function getManholeDevices(manholeId) {
+  return request({ url: '/api/manhole/device/list/' + manholeId, method: 'get' })
+}
+export function addManholeDeviceRel(data) {
+  return request({ url: '/api/manhole/device/save', method: 'post', data })
+}
+export function addManholeDeviceRelBatch(dataList) {
+  return request({ url: '/api/manhole/device/save-batch', method: 'post', data: dataList })
+}
+export function deleteManholeDeviceRel(relId) {
+  return request({ url: '/api/manhole/device/delete', method: 'delete', params: { relId } })
+}
+
 // ============ 维修记录 ============
 export function getRepairRecordPage(pageNum, pageSize, params) {
   return request({ url: '/api/repair-record/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
@@ -64,6 +78,9 @@ export function deleteRepairRecord(id) {
 }
 
 // ============ 监测设备管理 ============
+export function getEquipmentList() {
+  return request({ url: '/EquipmentBase/getList', method: 'get' })
+}
 export function getEquipmentPage(pageNum, pageSize, params) {
   return request({ url: '/EquipmentBase/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
 }
@@ -91,6 +108,9 @@ export function getEquipmentAbnormalList() {
 export function getPointRelByEquipment(equipmentId) {
   return request({ url: '/api/equipment/point-rel/' + equipmentId, method: 'get' })
 }
+export function getEquipmentByPoint(pointId) {
+  return request({ url: '/api/equipment/point-rel/by-point/' + pointId, method: 'get' })
+}
 // ============ 管点-设备关联 ============
 export function getPipePointDevices(pointId) {
   return request({ url: '/api/gas-pipe-point/device/list/' + pointId, method: 'get' })

+ 10 - 10
src/views/subSystem/basic/Equipment.vue

@@ -79,8 +79,8 @@
       />
     </el-card>
 
-    <el-dialog title="设备详情" v-model="detailVisible" width="750px">
-      <el-descriptions v-if="detailData" :column="2" border size="small">
+    <el-dialog title="设备详情" v-model="detailVisible" width="1100px">
+      <el-descriptions v-if="detailData" :column="2" border>
         <el-descriptions-item label="设备编码" :span="1">{{ detailData.equipmentCode }}</el-descriptions-item>
         <el-descriptions-item label="设备名称" :span="1">{{ detailData.equipmentName }}</el-descriptions-item>
         <el-descriptions-item label="设备型号">{{ detailData.equipmentModel }}</el-descriptions-item>
@@ -117,9 +117,9 @@
       </el-descriptions>
     </el-dialog>
 
-    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="650px" destroy-on-close>
-      <el-form :model="formData" ref="formRef" label-width="100px">
-        <el-row :gutter="20">
+    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="1100px" destroy-on-close>
+      <el-form :model="formData" ref="formRef" label-width="110px" size="default">
+        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="设备编码" prop="equipmentCode">
               <el-input v-model="formData.equipmentCode" placeholder="请输入设备编码" />
@@ -131,7 +131,7 @@
             </el-form-item>
           </el-col>
         </el-row>
-        <el-row :gutter="20">
+        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="设备型号">
               <el-input v-model="formData.equipmentModel" />
@@ -143,7 +143,7 @@
             </el-form-item>
           </el-col>
         </el-row>
-        <el-row :gutter="20">
+        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="制造商">
               <el-input v-model="formData.manufacturer" />
@@ -155,7 +155,7 @@
             </el-form-item>
           </el-col>
         </el-row>
-        <el-row :gutter="20">
+        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="经度">
               <el-input-number v-model="formData.longitude" :precision="10" style="width:100%" />
@@ -167,7 +167,7 @@
             </el-form-item>
           </el-col>
         </el-row>
-        <el-row :gutter="20">
+        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="维护人员">
               <el-input v-model="formData.maintainer" />
@@ -180,7 +180,7 @@
           </el-col>
         </el-row>
         <el-form-item label="备注">
-          <el-input v-model="formData.remark" type="textarea" :rows="2" />
+          <el-input v-model="formData.remark" type="textarea" :rows="3" />
         </el-form-item>
       </el-form>
       <template #footer>

+ 128 - 5
src/views/subSystem/basic/Manhole.vue

@@ -41,9 +41,10 @@
             <el-tag :type="row.status === '0' ? 'success' : 'danger'">{{ getDictLabel(pipeNetworkStatusDict, row.status) }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="180" fixed="right">
+        <el-table-column label="操作" width="240" fixed="right">
           <template #default="{ row }">
             <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
+            <el-button type="success" size="small" @click="handleDeviceDialog(row)">关联设备</el-button>
             <el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
           </template>
         </el-table-column>
@@ -59,6 +60,45 @@
       <div id="manholeMap" style="width: 100%; height: 600px"></div>
     </el-card>
 
+    <el-dialog title="关联监测设备" v-model="deviceDialogVisible" width="950px" destroy-on-close>
+      <el-form label-width="80px">
+        <el-form-item label="当前窨井">
+          <span>{{ currentManhole.manholeName || '-' }}</span>
+        </el-form-item>
+        <el-form-item label="选择设备">
+          <el-select
+            v-model="bindForm.equipmentId"
+            filterable
+            clearable
+            placeholder="请选择要关联的设备"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in unboundEquipmentOptions"
+              :key="item.equipmentId"
+              :label="`${item.equipmentName}(${item.equipmentCode})`"
+              :value="item.equipmentId"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div style="margin-bottom: 12px">
+        <el-button type="primary" @click="submitBind">添加关联</el-button>
+      </div>
+      <el-table :data="deviceList" border stripe v-loading="deviceLoading">
+        <el-table-column label="设备编码" prop="equipmentCode" min-width="140" />
+        <el-table-column label="设备名称" prop="equipmentName" min-width="180" show-overflow-tooltip />
+        <el-table-column label="设备型号" prop="equipmentModel" min-width="120" />
+        <el-table-column label="制造商" prop="manufacturer" min-width="140" show-overflow-tooltip />
+        <el-table-column label="设备位置" prop="equipmentLocation" min-width="180" show-overflow-tooltip />
+        <el-table-column label="操作" align="center" width="100">
+          <template #default="{ row: dev }">
+            <el-button link type="danger" icon="Close" @click="handleUnbind(dev)">解绑</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
+
     <el-dialog :title="dialogTitle" v-model="dialogVisible" width="650px" destroy-on-close>
       <el-form :model="formData" ref="formRef" label-width="100px">
         <el-form-item label="窨井编码" prop="manholeCode">
@@ -111,9 +151,13 @@
 </template>
 
 <script setup>
-import { ref, reactive, onMounted, nextTick, watch } from 'vue'
+import { ref, reactive, computed, onMounted, nextTick, watch } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { getManholePage, addManhole, updateManhole, deleteManhole } from '@/api/pipeNetwork/basic'
+import {
+  getManholePage, addManhole, updateManhole, deleteManhole,
+  getManholeDevices, addManholeDeviceRel, deleteManholeDeviceRel,
+  getEquipmentList
+} from '@/api/pipeNetwork/basic'
 import { getDicts } from '@/api/system/dict/data'
 
 const loading = ref(false)
@@ -135,6 +179,17 @@ const formData = reactive({
 
 const pipeNetworkStatusDict = ref([])
 const manholeShapeDict = ref([])
+
+const deviceDialogVisible = ref(false)
+const currentManhole = reactive({ manholeId: '', manholeName: '' })
+const deviceList = ref([])
+const deviceLoading = ref(false)
+const bindForm = reactive({ equipmentId: '' })
+const equipmentOptions = ref([])
+const unboundEquipmentOptions = computed(() => {
+  const boundIds = deviceList.value.map(d => d.equipmentId)
+  return equipmentOptions.value.filter(e => !boundIds.includes(e.equipmentId))
+})
 function getDictLabel(dict, value) {
   const item = dict.find(d => d.dictValue === value)
   return item ? item.dictLabel : value
@@ -179,15 +234,83 @@ function handleDelete(row) {
   }).catch(() => {})
 }
 
+async function handleDeviceDialog(row) {
+  Object.assign(currentManhole, { manholeId: row.manholeId, manholeName: row.manholeName })
+  bindForm.equipmentId = ''
+  deviceDialogVisible.value = true
+  if (equipmentOptions.value.length === 0) { await loadEquipmentOptions() }
+  loadDeviceList()
+}
+async function loadDeviceList() {
+  deviceLoading.value = true
+  try {
+    const res = await getManholeDevices(currentManhole.manholeId)
+    const raw = res.data || []
+    deviceList.value = raw.map(r => ({
+      relId: r.relId,
+      equipmentId: r.equipmentId,
+      equipmentName: r.equipmentName,
+      equipmentType: r.equipmentType,
+      equipmentCode: '',
+      equipmentModel: '',
+      manufacturer: '',
+      equipmentLocation: ''
+    }))
+    if (raw.length > 0) {
+      const eqMap = new Map(equipmentOptions.value.map(e => [e.equipmentId, e]))
+      deviceList.value = raw.map(r => {
+        const full = eqMap.get(r.equipmentId)
+        return {
+          relId: r.relId,
+          equipmentId: r.equipmentId,
+          equipmentName: r.equipmentName || (full ? full.equipmentName : ''),
+          equipmentCode: full ? full.equipmentCode : '',
+          equipmentModel: full ? full.equipmentModel : '',
+          manufacturer: full ? full.manufacturer : '',
+          equipmentLocation: full ? full.equipmentLocation : ''
+        }
+      })
+    }
+  } catch (e) { console.error(e) } finally { deviceLoading.value = false }
+}
+async function loadEquipmentOptions() {
+  try {
+    const res = await getEquipmentList()
+    equipmentOptions.value = res.data || []
+  } catch (e) { console.error(e) }
+}
+async function submitBind() {
+  if (!bindForm.equipmentId) { ElMessage.warning('请先选择设备'); return }
+  const eq = equipmentOptions.value.find(e => e.equipmentId === bindForm.equipmentId)
+  try {
+    await addManholeDeviceRel({
+      manholeId: currentManhole.manholeId,
+      equipmentId: bindForm.equipmentId,
+      equipmentName: eq ? eq.equipmentName : '',
+      equipmentType: eq ? (eq.equipmentTypeId || '') : ''
+    })
+    ElMessage.success('关联成功')
+    bindForm.equipmentId = ''
+    loadDeviceList()
+  } catch (e) { console.error(e) }
+}
+function handleUnbind(dev) {
+  ElMessageBox.confirm('确认解除该设备关联?', '提示', { type: 'warning' }).then(async () => {
+    await deleteManholeDeviceRel(dev.relId)
+    ElMessage.success('解绑成功')
+    loadDeviceList()
+  }).catch(() => {})
+}
+
 async function initMap() {
   await nextTick()
   const el = document.getElementById('manholeMap')
   if (!el) return
-  try {
+ /* try {
     const L = await import('leaflet')
   } catch (e) {
     el.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#999;">地图加载需要Leaflet依赖,请安装: npm install leaflet</div>'
-  }
+  }*/
 }
 watch(viewMode, (v) => { if (v === 'map') initMap() })
 

+ 100 - 4
src/views/subSystem/basic/PointManage.vue

@@ -47,6 +47,45 @@
       />
     </el-card>
 
+    <el-dialog title="关联监测设备" v-model="deviceDialogVisible" width="950px" destroy-on-close>
+      <el-form label-width="80px">
+        <el-form-item label="当前测点">
+          <span>{{ currentPoint.pointName || '-' }}</span>
+        </el-form-item>
+        <el-form-item label="选择设备">
+          <el-select
+            v-model="bindForm.equipmentId"
+            filterable
+            clearable
+            placeholder="请选择要关联的设备"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in unboundEquipmentOptions"
+              :key="item.equipmentId"
+              :label="`${item.equipmentName}(${item.equipmentCode})`"
+              :value="item.equipmentId"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div style="margin-bottom: 12px">
+        <el-button type="primary" @click="submitBind">添加关联</el-button>
+      </div>
+      <el-table :data="deviceList" border stripe v-loading="deviceLoading">
+        <el-table-column label="设备编码" prop="equipmentCode" min-width="140" />
+        <el-table-column label="设备名称" prop="equipmentName" min-width="180" show-overflow-tooltip />
+        <el-table-column label="设备型号" prop="equipmentModel" min-width="120" />
+        <el-table-column label="制造商" prop="manufacturer" min-width="140" show-overflow-tooltip />
+        <el-table-column label="设备位置" prop="equipmentLocation" min-width="180" show-overflow-tooltip />
+        <el-table-column label="操作" align="center" width="100">
+          <template #default="{ row: dev }">
+            <el-button link type="danger" icon="Close" @click="handleUnbind(dev)">解绑</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
+
     <el-dialog :title="dialogTitle" v-model="dialogVisible" width="550px" destroy-on-close>
       <el-form :model="formData" ref="formRef" label-width="100px">
         <el-form-item label="测点编码" prop="pointCode">
@@ -87,9 +126,9 @@
 </template>
 
 <script setup>
-import { ref, reactive, onMounted } from 'vue'
+import { ref, reactive, computed, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { getPointPage, addPoint, updatePoint, deletePoint } from '@/api/pipeNetwork/basic'
+import { getPointPage, addPoint, updatePoint, deletePoint, getEquipmentByPoint, savePointRel, deletePointRel, getEquipmentList } from '@/api/pipeNetwork/basic'
 import { getDicts } from '@/api/system/dict/data'
 
 const loading = ref(false)
@@ -109,6 +148,17 @@ const formData = reactive({
 const pointTypeDict = ref([])
 const pointStatusDict = ref([])
 
+const deviceDialogVisible = ref(false)
+const currentPoint = reactive({ pointId: '', pointName: '' })
+const deviceList = ref([])
+const deviceLoading = ref(false)
+const bindForm = reactive({ equipmentId: '' })
+const equipmentOptions = ref([])
+const unboundEquipmentOptions = computed(() => {
+  const boundIds = deviceList.value.map(d => d.equipmentId)
+  return equipmentOptions.value.filter(e => !boundIds.includes(e.equipmentId))
+})
+
 function getDictLabel(dict, value) {
   const item = dict.find(d => d.dictValue === String(value))
   return item ? item.dictLabel : value
@@ -151,8 +201,54 @@ function handleDelete(row) {
     await deletePoint(row.pointId); ElMessage.success('删除成功'); loadData()
   }).catch(() => {})
 }
-function handleRelDevice(row) {
-  ElMessage.info('设备关联功能待实现')
+async function handleRelDevice(row) {
+  Object.assign(currentPoint, { pointId: row.pointId, pointName: row.pointName })
+  bindForm.equipmentId = ''
+  deviceDialogVisible.value = true
+  if (equipmentOptions.value.length === 0) { await loadEquipmentOptions() }
+  loadDeviceList()
+}
+async function loadDeviceList() {
+  deviceLoading.value = true
+  try {
+    const res = await getEquipmentByPoint(currentPoint.pointId)
+    const raw = res.data || []
+    const eqMap = new Map(equipmentOptions.value.map(e => [e.equipmentId, e]))
+    deviceList.value = raw.map(r => {
+      const full = eqMap.get(r.equipmentId)
+      return {
+        relId: r.relId,
+        equipmentId: r.equipmentId,
+        equipmentName: full ? full.equipmentName : '',
+        equipmentCode: full ? full.equipmentCode : '',
+        equipmentModel: full ? full.equipmentModel : '',
+        manufacturer: full ? full.manufacturer : '',
+        equipmentLocation: full ? full.equipmentLocation : ''
+      }
+    })
+  } catch (e) { console.error(e) } finally { deviceLoading.value = false }
+}
+async function loadEquipmentOptions() {
+  try {
+    const res = await getEquipmentList()
+    equipmentOptions.value = res.data || []
+  } catch (e) { console.error(e) }
+}
+async function submitBind() {
+  if (!bindForm.equipmentId) { ElMessage.warning('请先选择设备'); return }
+  try {
+    await savePointRel({ equipmentId: bindForm.equipmentId, pointId: currentPoint.pointId })
+    ElMessage.success('关联成功')
+    bindForm.equipmentId = ''
+    loadDeviceList()
+  } catch (e) { console.error(e) }
+}
+function handleUnbind(dev) {
+  ElMessageBox.confirm('确认解除该设备关联?', '提示', { type: 'warning' }).then(async () => {
+    await deletePointRel(dev.relId)
+    ElMessage.success('解绑成功')
+    loadDeviceList()
+  }).catch(() => {})
 }
 
 onMounted(async () => { await loadDicts(); loadData() })

+ 8 - 7
src/views/subSystem/gas/gasOperamon/gxlljc.vue

@@ -127,13 +127,7 @@
           <div class="table-card">
             <div class="table-header">
               <span>历史监测记录</span>
-              <el-pagination
-                v-model:current-page="historyPage"
-                v-model:page-size="historyPageSize"
-                :total="historyData.length"
-                layout="total, prev, pager, next"
-                small
-              />
+
             </div>
             <el-table :data="pagedHistory" border stripe size="small">
               <el-table-column prop="time" label="采集时间" width="160" />
@@ -157,6 +151,13 @@
               <el-table-column prop="collector" label="采集设备" min-width="140" />
               <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
             </el-table>
+            <el-pagination
+                v-model:current-page="historyPage"
+                v-model:page-size="historyPageSize"
+                :total="historyData.length"
+                layout="total, prev, pager, next"
+                small
+            />
           </div>
         </template>
 

+ 8 - 7
src/views/subSystem/gas/gasOperamon/gxyljc.vue

@@ -125,13 +125,6 @@
           <div class="table-card">
             <div class="table-header">
               <span>历史监测记录</span>
-              <el-pagination
-                v-model:current-page="historyPage"
-                v-model:page-size="historyPageSize"
-                :total="historyData.length"
-                layout="total, prev, pager, next"
-                small
-              />
             </div>
             <el-table :data="pagedHistory" border stripe size="small">
               <el-table-column prop="time" label="采集时间" width="160" />
@@ -153,7 +146,15 @@
               <el-table-column prop="collector" label="采集设备" min-width="140" />
               <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
             </el-table>
+            <el-pagination
+                v-model:current-page="historyPage"
+                v-model:page-size="historyPageSize"
+                :total="historyData.length"
+                layout="total, prev, pager, next"
+                small
+            />
           </div>
+
         </template>
 
         <!-- 未选中状态 -->

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

@@ -1,16 +1,17 @@
 <script setup>
 import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
 import * as echarts from 'echarts'
+import { Odometer, CircleCheckFilled, VideoCameraFilled, WarnTriangleFilled } from '@element-plus/icons-vue'
 
 const nowText = ref('2025/12/31 12:59:59')
 const weekText = ref('星期三')
 const mapError = ref('')
 
 const baseCards = computed(() => [
-  { label: '监测点总数', value: 18, unit: '个' },
-  { label: '在线监测点', value: 16, unit: '个' },
-  { label: '离线监测点', value: 2, unit: '个' },
-  { label: '当日预警点', value: 3, unit: '个' }
+  { label: '监测点总数', value: 18, unit: '个', icon: Odometer, color: '#60b8ff' },
+  { label: '在线监测点', value: 16, unit: '个', icon: CircleCheckFilled, color: '#52e0a8' },
+  { label: '离线监测点', value: 2, unit: '个', icon: VideoCameraFilled, color: '#ff8e8e' },
+  { label: '当日预警点', value: 3, unit: '个', icon: WarnTriangleFilled, color: '#ffb54d' }
 ])
 
 const operationSummary = [
@@ -340,7 +341,9 @@ onBeforeUnmount(() => {
           <div class="panel-title">基础概况</div>
           <div class="base-grid">
             <div v-for="item in baseCards" :key="item.label" class="base-item">
-              <div class="base-icon"></div>
+              <div class="base-icon" :style="{ background: item.color }">
+                <component :is="item.icon" class="base-icon-svg" />
+              </div>
               <div class="base-text">
                 <div class="base-label">{{ item.label }}</div>
                 <div class="base-value">{{ item.value }}<span>{{ item.unit }}</span></div>
@@ -627,11 +630,18 @@ onBeforeUnmount(() => {
   width: 48px;
   height: 48px;
   border-radius: 16px;
-  background: linear-gradient(180deg, rgba(108, 233, 255, 0.96), rgba(26, 111, 245, 0.96));
-  box-shadow: 0 0 18px rgba(30, 168, 255, 0.28);
+  display: flex;
+  align-items: center;
+  justify-content: center;
   flex: 0 0 48px;
 }
 
+.base-icon-svg {
+  width: 26px;
+  height: 26px;
+  color: #fff;
+}
+
 .base-text {
   min-width: 0;
 }