| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586 |
- <!--监测设备台账(窨井设备)-->
- <template>
- <div class="manhole-cover-management">
- <!-- 页面头部 -->
- <div class="page-header">
- <div class="stats-section">
- <el-card class="stat-card" shadow="hover">
- <div class="stat-content">
- <el-icon><Monitor /></el-icon>
- <div>
- <div class="stat-label">在线设备</div>
- <div class="stat-number">{{ onlineDevicesCount }}</div>
- </div>
- </div>
- </el-card>
- <el-card class="stat-card" shadow="hover">
- <div class="stat-content">
- <el-icon><CircleClose /></el-icon>
- <div>
- <div class="stat-label">离线设备</div>
- <div class="stat-number">{{ offlineDevicesCount }}</div>
- </div>
- </div>
- </el-card>
- <el-card class="stat-card" shadow="hover">
- <div class="stat-content">
- <el-icon><Location /></el-icon>
- <div>
- <div class="stat-label">设备总数</div>
- <div class="stat-number">{{ totalDevicesCount }}</div>
- </div>
- </div>
- </el-card>
- </div>
- </div>
- <!-- 筛选工具栏 -->
- <div class="filter-bar">
- <el-input
- v-model="searchKeyword"
- placeholder="搜索设备名称"
- clearable
- style="width: 240px"
- :prefix-icon="Search"
- @keyup.enter="handleSearch"
- @clear="handleSearch"
- />
- <el-select v-model="typeFilter" placeholder="设备类型" clearable style="width: 160px" @change="handleSearch">
- <el-option v-for="t in typeOptions" :key="t.value" :label="t.label" :value="t.value" />
- </el-select>
- <el-button type="primary" :icon="Refresh" @click="resetFilters">重置筛选</el-button>
- </div>
- <!-- 设备列表展示区 -->
- <div class="device-list-container">
- <el-table
- :data="devices"
- v-loading="loading"
- stripe
- border
- style="width: 100%"
- :row-class-name="tableRowClassName"
- @row-click="handleRowClick"
- >
- <el-table-column prop="deviceCode" label="设备编号" width="150" show-overflow-tooltip />
- <el-table-column prop="deviceName" label="设备名称" width="180" show-overflow-tooltip />
- <el-table-column prop="typeName" label="设备类型" width="130">
- <template #default="{ row }">
- <el-tag size="small" effect="plain">{{ getTypeText(row) }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="location" label="设备位置" min-width="200" show-overflow-tooltip />
- <el-table-column prop="lastMaintainDate" label="最近运维时间" width="130">
- <template #default="{ row }">{{ row.lastMaintainDate || '—' }}</template>
- </el-table-column>
- <el-table-column prop="onlineStatus" label="在线状态" width="110" align="center">
- <template #default="{ row }">
- <el-tag :type="getOnlineTagType(row.onlineStatus)" size="small">{{ getOnlineText(row.onlineStatus) }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="180" fixed="right">
- <template #default="{ row }">
- <el-button link type="primary" size="small" @click.stop="showDetail(row)">详情</el-button>
- <el-button link type="success" size="small" @click.stop="openRelDialog(row)">关联井下设备</el-button>
- </template>
- </el-table-column>
- </el-table>
- <!-- 分页 -->
- <div class="pagination-wrapper">
- <el-pagination
- background
- layout="total, prev, pager, next"
- :total="total"
- :page-size="pageSize"
- v-model:current-page="currentPage"
- @current-change="loadDevices"
- />
- </div>
- </div>
- <!-- 设备详情抽屉 -->
- <el-drawer v-model="drawerVisible" title="设备电子档案 - 详细信息" size="40%" direction="rtl" v-loading="detailLoading" class="detail-drawer">
- <template v-if="selectedDevice">
- <el-descriptions :column="1" border>
- <el-descriptions-item label="设备编号">{{ selectedDevice.deviceCode }}</el-descriptions-item>
- <el-descriptions-item label="设备名称">{{ selectedDevice.deviceName }}</el-descriptions-item>
- <el-descriptions-item label="设备类型">{{ getTypeText(selectedDevice) }}</el-descriptions-item>
- <el-descriptions-item label="在线状态">
- <el-tag :type="getOnlineTagType(selectedDevice.onlineStatus)">{{ getOnlineText(selectedDevice.onlineStatus) }}</el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="设备位置">{{ selectedDevice.location || '—' }}</el-descriptions-item>
- <el-descriptions-item label="最近运维时间">{{ selectedDevice.lastMaintainDate || '—' }}</el-descriptions-item>
- <el-descriptions-item label="电池电量/健康度" v-if="selectedDevice.batteryLevel !== null && selectedDevice.batteryLevel !== undefined && selectedDevice.batteryLevel !== ''">{{ selectedDevice.batteryLevel }}%</el-descriptions-item>
- <el-descriptions-item label="最后通讯时间">{{ selectedDevice.lastCommunication || '—' }}</el-descriptions-item>
- </el-descriptions>
- <el-divider content-position="left">动态监测指标</el-divider>
- <div class="monitor-metrics">
- <el-row :gutter="12">
- <el-col :span="12"><el-statistic title="井盖倾斜角" :value="selectedDevice.metrics?.tilt ?? '—'" suffix="°" /></el-col>
- <el-col :span="12"><el-statistic title="水浸状态" :value="selectedDevice.metrics?.waterIntrusion ? '已浸水' : '正常'" /></el-col>
- <el-col :span="12"><el-statistic title="温度" :value="selectedDevice.metrics?.temperature ?? '—'" suffix="℃" /></el-col>
- <el-col :span="12"><el-statistic title="信号量" :value="selectedDevice.metrics?.signal ?? '—'" /></el-col>
- </el-row>
- </div>
- <el-divider content-position="left">预警阈值</el-divider>
- <el-table v-if="selectedDevice.warningThresholds.length" :data="selectedDevice.warningThresholds" size="small" border>
- <el-table-column prop="warningType" label="预警类型" width="100" />
- <el-table-column prop="warningCode" label="预警编码" width="120" />
- <el-table-column prop="minValue" label="最小值" width="90" />
- <el-table-column prop="maxValue" label="最大值" width="90" />
- <el-table-column prop="remark" label="备注" show-overflow-tooltip />
- </el-table>
- <div v-else style="color:#909399;font-size:13px;">暂无预警阈值配置</div>
- <div style="margin-top: 20px; text-align: right;">
- <el-button type="primary" @click="closeDrawer">关闭</el-button>
- </div>
- </template>
- </el-drawer>
- <!-- 关联设备管理对话框 -->
- <el-dialog v-model="relDialogVisible" :title="`井下设备关联 - ${relDevice?.deviceName || ''}`" width="860px" @close="onRelDialogClose">
- <div class="rel-container">
- <!-- 已关联设备列表 -->
- <div class="rel-section">
- <div class="rel-section-title">
- <span>已关联设备</span>
- <el-tag type="info" size="small">{{ relList.length }} 台</el-tag>
- </div>
- <el-table :data="relList" border size="small" v-loading="relLoading" max-height="240">
- <el-table-column prop="equipmentCode" label="设备编号" min-width="140" show-overflow-tooltip />
- <el-table-column prop="equipmentName" label="设备名称" min-width="140" show-overflow-tooltip />
- <el-table-column prop="equipmentLocation" label="位置" min-width="160" show-overflow-tooltip />
- <el-table-column label="操作" width="90" align="center">
- <template #default="{ row }">
- <el-button link type="danger" size="small" @click="removeRel(row)">取消关联</el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <!-- 搜索可选设备 -->
- <div class="rel-section">
- <div class="rel-section-title">
- <span>添加关联设备</span>
- <el-input
- v-model="relSearchKeyword"
- placeholder="搜索设备编号/名称/位置"
- clearable
- size="small"
- style="width: 260px"
- :prefix-icon="Search"
- @keyup.enter="searchRelDevices"
- @clear="searchRelDevices"
- />
- </div>
- <el-table :data="searchResults" border size="small" v-loading="relSearching" max-height="240" @selection-change="handleSearchSelectionChange">
- <el-table-column type="selection" width="45" />
- <el-table-column prop="equipmentCode" label="设备编号" min-width="140" show-overflow-tooltip />
- <el-table-column prop="equipmentName" label="设备名称" min-width="140" show-overflow-tooltip />
- <el-table-column prop="equipmentLocation" label="位置" min-width="160" show-overflow-tooltip />
- <el-table-column label="类型" width="120">
- <template #default="{ row }">
- <el-tag size="small" effect="plain">{{ getTypeText(row) }}</el-tag>
- </template>
- </el-table-column>
- </el-table>
- <div class="rel-search-footer">
- <el-button type="primary" size="small" :disabled="!searchSelection.length" :loading="relSaving" @click="batchAddRel">关联选中设备 ({{ searchSelection.length }})</el-button>
- </div>
- </div>
- </div>
- </el-dialog>
- </div>
- </template>
- <script setup>
- import { ref, onMounted } from 'vue'
- import { Monitor, CircleClose, Location, Search, Refresh } from '@element-plus/icons-vue'
- import { getManholeDevicePage, getManholeDeviceById, getEquipmentTypeList, searchManholeRelDevices, getManholeDeviceRels, addManholeDeviceRel, deleteManholeDeviceRel } from '@/api/pipeNetwork/basic'
- import { ElMessage } from 'element-plus'
- // 窨井设备类别:供水窨井(type_id=5)/ 排水窨井(type_id=9)/ 燃气窨井(type_id=10)
- const MANHOLE_CATEGORY_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
- // 设备列表 & 分页
- const devices = ref([])
- const loading = ref(false)
- const total = ref(0)
- const currentPage = ref(1)
- const pageSize = 10
- // 筛选条件
- const searchKeyword = ref('')
- const typeFilter = ref('')
- // 窨井类别选项(value=窨井类别ID:供水窨井5/排水窨井9/燃气窨井10)
- const typeOptions = ref([
- { value: '5', label: '供水窨井' },
- { value: '9', label: '排水窨井' },
- { value: '10', label: '燃气窨井' }
- ])
- // 全部类型:id -> { typeId, typeName, parentTypeId }
- const typeIdMap = ref({})
- // 统计
- const onlineDevicesCount = ref(0)
- const offlineDevicesCount = ref(0)
- const totalDevicesCount = ref(0)
- // 加载全部设备类型(用于类型列归属显示)
- async function loadTypeOptions() {
- try {
- const res = await getEquipmentTypeList()
- const all = res.data || []
- all.forEach(t => { typeIdMap.value[t.id] = t })
- } catch (e) { /* 类型接口异常时,类型列回退显示 typeName */ }
- }
- // 设备类型显示文本(按所属窨井类别显示)
- const getTypeText = (row) => {
- const t = typeIdMap.value[row.equipmentTypeId]
- if (!t) return row.typeName || '—'
- return MANHOLE_CATEGORY_MAP[t.parentTypeId] || MANHOLE_CATEGORY_MAP[row.equipmentTypeId] || t.typeName || '—'
- }
- // 加载设备列表(后端已按窨井设备类型过滤)
- async function loadDevices() {
- loading.value = true
- try {
- const res = await getManholeDevicePage({
- pageNum: currentPage.value,
- pageSize,
- deviceName: searchKeyword.value || null,
- equipmentTypeId: typeFilter.value || null,
- isQueryManholeData: true
- })
- const rows = res.data?.records || []
- devices.value = rows.map(d => ({
- equipmentId: d.equipmentId,
- equipmentTypeId: d.equipmentTypeId,
- deviceCode: d.equipmentCode,
- deviceName: d.equipmentName,
- typeName: d.equipmentTypeName,
- location: d.equipmentLocation,
- onlineStatus: d.equipmentStatus?.onlineStatus,
- lastMaintainDate: d.equipmentStatus?.lastMaintainDate,
- batteryLevel: d.manholeData?.batteryLevel,
- lastCommunication: d.manholeData?.uploadTime || d.manholeData?.createTime,
- metrics: d.manholeData ? {
- tilt: d.manholeData.tiltAngle,
- waterIntrusion: Number(d.manholeData.waterInfiltrationAlarmStatus) === 1,
- temperature: d.manholeData.temperatureValue,
- signal: d.manholeData.signalStrength
- } : null
- }))
- total.value = res.data?.total || 0
- } catch (e) {
- ElMessage.error('设备列表加载失败')
- } finally {
- loading.value = false
- }
- }
- // 统计(基于当前筛选条件下的全量设备)
- async function loadStats() {
- try {
- const res = await getManholeDevicePage({
- pageNum: 1,
- pageSize: 1000,
- deviceName: searchKeyword.value || null,
- equipmentTypeId: typeFilter.value || null,
- isQueryManholeData: false
- })
- const rows = res.data?.records || []
- totalDevicesCount.value = res.data?.total || rows.length
- onlineDevicesCount.value = rows.filter(d => Number(d.equipmentStatus?.onlineStatus) === 1).length
- offlineDevicesCount.value = rows.length - onlineDevicesCount.value
- } catch (e) {
- onlineDevicesCount.value = 0
- offlineDevicesCount.value = 0
- totalDevicesCount.value = 0
- }
- }
- // 在线状态
- const getOnlineText = (status) => {
- return Number(status) === 1 ? '在线' : '离线'
- }
- const getOnlineTagType = (status) => {
- return Number(status) === 1 ? 'success' : 'info'
- }
- // 搜索/筛选变化
- const handleSearch = () => {
- currentPage.value = 1
- loadDevices()
- loadStats()
- }
- const resetFilters = () => {
- searchKeyword.value = ''
- typeFilter.value = ''
- currentPage.value = 1
- loadDevices()
- loadStats()
- }
- const tableRowClassName = ({ row }) => {
- if (Number(row.onlineStatus) !== 1) return 'inactive-row'
- return ''
- }
- // 详情
- const drawerVisible = ref(false)
- const detailLoading = ref(false)
- const selectedDevice = ref(null)
- // 映射详情接口返回数据
- const mapDeviceDetail = (d) => ({
- equipmentId: d.equipmentId,
- equipmentTypeId: d.equipmentTypeId,
- deviceCode: d.equipmentCode,
- deviceName: d.equipmentName,
- typeName: d.equipmentTypeName,
- location: d.equipmentLocation,
- onlineStatus: d.equipmentStatus?.onlineStatus,
- lastMaintainDate: d.equipmentStatus?.lastMaintainDate,
- batteryLevel: d.manholeData?.batteryLevel,
- lastCommunication: d.manholeData?.uploadTime || d.manholeData?.createTime,
- metrics: d.manholeData ? {
- tilt: d.manholeData.tiltAngle,
- waterIntrusion: Number(d.manholeData.waterInfiltrationAlarmStatus) === 1,
- temperature: d.manholeData.temperatureValue,
- signal: d.manholeData.signalStrength
- } : null,
- warningThresholds: d.warningThresholdList || []
- })
- const showDetail = async (device) => {
- drawerVisible.value = true
- detailLoading.value = true
- try {
- const res = await getManholeDeviceById(device.equipmentId)
- selectedDevice.value = res.data ? mapDeviceDetail(res.data) : null
- } catch (e) {
- selectedDevice.value = null
- ElMessage.error('设备详情加载失败')
- } finally {
- detailLoading.value = false
- }
- }
- const handleRowClick = (row) => {
- showDetail(row)
- }
- const closeDrawer = () => {
- drawerVisible.value = false
- }
- // ============ 关联设备管理 ============
- const relDialogVisible = ref(false)
- const relDevice = ref(null)
- const relList = ref([])
- const relLoading = ref(false)
- const relSearchKeyword = ref('')
- const searchResults = ref([])
- const relSearching = ref(false)
- const searchSelection = ref([])
- const relSaving = ref(false)
- const openRelDialog = async (device) => {
- relDevice.value = device
- relDialogVisible.value = true
- relSearchKeyword.value = ''
- searchResults.value = []
- searchSelection.value = []
- await loadRelList(device.equipmentId)
- await searchRelDevices()
- }
- const loadRelList = async (equipmentId) => {
- relLoading.value = true
- try {
- const res = await getManholeDeviceRels(equipmentId)
- relList.value = res.data || []
- } catch (e) {
- relList.value = []
- ElMessage.error('关联设备列表加载失败')
- } finally {
- relLoading.value = false
- }
- }
- const searchRelDevices = async () => {
- relSearching.value = true
- try {
- const keyword = relSearchKeyword.value.trim()
- // 排除已关联设备ID + 自身设备ID
- const excludeIds = [
- ...relList.value.map(r => r.equipmentId),
- relDevice.value?.equipmentId
- ].filter(Boolean).join(',')
- const res = await searchManholeRelDevices({
- keyword: keyword || undefined,
- excludeIds: excludeIds || undefined
- })
- searchResults.value = res.data || []
- } catch (e) {
- searchResults.value = []
- ElMessage.error('设备搜索失败')
- } finally {
- relSearching.value = false
- }
- }
- const handleSearchSelectionChange = (selection) => {
- searchSelection.value = selection || []
- }
- const batchAddRel = async () => {
- if (!searchSelection.value.length) return
- relSaving.value = true
- try {
- for (const item of searchSelection.value) {
- await addManholeDeviceRel({
- manholeId: relDevice.value.equipmentId,
- equipmentId: item.equipmentId
- })
- }
- ElMessage.success(`已关联 ${searchSelection.value.length} 台设备`)
- searchSelection.value = []
- relSearchKeyword.value = ''
- await loadRelList(relDevice.value.equipmentId)
- await searchRelDevices()
- } catch (e) {
- const msg = e?.message || e?.msg || '关联失败'
- ElMessage.error(msg)
- } finally {
- relSaving.value = false
- }
- }
- const removeRel = async (row) => {
- try {
- await deleteManholeDeviceRel(row.relId)
- ElMessage.success('已取消关联')
- await loadRelList(relDevice.value.equipmentId)
- await searchRelDevices()
- } catch (e) {
- const msg = e?.message || e?.msg || '取消关联失败'
- ElMessage.error(msg)
- }
- }
- const onRelDialogClose = () => {
- relList.value = []
- searchResults.value = []
- searchSelection.value = []
- relSearchKeyword.value = ''
- }
- // 初始化
- onMounted(() => {
- loadTypeOptions()
- loadDevices()
- loadStats()
- })
- </script>
- <style scoped>
- .manhole-cover-management {
- padding: 20px;
- background: #f5f7fa;
- min-height: 100vh;
- }
- .page-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
- flex-wrap: wrap;
- }
- .stats-section {
- display: flex;
- gap: 16px;
- }
- .stat-card {
- width: 160px;
- border-radius: 10px;
- }
- .stat-content {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .stat-content .el-icon {
- font-size: 32px;
- color: #409eff;
- }
- .stat-label {
- font-size: 14px;
- color: #6c757d;
- }
- .stat-number {
- font-size: 24px;
- font-weight: bold;
- color: #2c3e50;
- }
- .filter-bar {
- margin-bottom: 20px;
- display: flex;
- gap: 12px;
- flex-wrap: wrap;
- background: white;
- padding: 12px 16px;
- border-radius: 12px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
- }
- .device-list-container {
- background: white;
- border-radius: 12px;
- box-shadow: 0 2px 12px rgba(0,0,0,0.08);
- overflow: hidden;
- }
- .pagination-wrapper {
- display: flex;
- justify-content: flex-end;
- padding: 15px 20px;
- background: white;
- border-top: 1px solid #e4e7ed;
- }
- .monitor-metrics {
- background: #f8f9fc;
- border-radius: 8px;
- padding: 10px;
- }
- :deep(.inactive-row) {
- --el-table-tr-bg-color: #f5f5f5;
- color: #a8a8a8;
- }
- .rel-container {
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
- .rel-section {
- border: 1px solid #ebeef5;
- border-radius: 8px;
- padding: 12px;
- }
- .rel-section-title {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 10px;
- font-size: 14px;
- font-weight: 600;
- color: #2c3e50;
- }
- .rel-search-footer {
- margin-top: 10px;
- text-align: right;
- }
- </style>
|