更新
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="project-overview">
|
||||
<div class="main-panel">
|
||||
<div class="top-panel">
|
||||
<!-- 地图区域 -->
|
||||
<div class="overview-card">
|
||||
<div class="card-header">
|
||||
<div class="tabs">
|
||||
<div :class="['tab', { active: deviceTab === 'device' }]" @click="deviceTab = 'device'">设备概况</div>
|
||||
<div :class="['tab', { active: deviceTab === 'project' }]" @click="deviceTab = 'project'">项目概况</div>
|
||||
<!-- <div :class="['tab', { active: deviceTab === 'project' }]" @click="deviceTab = 'project'">项目概况</div> -->
|
||||
</div>
|
||||
<el-icon class="fullscreen-icon" @click="toggleMapFullscreen"><FullScreen /></el-icon>
|
||||
</div>
|
||||
@@ -15,159 +15,369 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 警情列表 -->
|
||||
<div class="alarm-list">
|
||||
<div class="list-header">
|
||||
<div class="list-title">
|
||||
<span class="dot"></span>
|
||||
警情列表
|
||||
<!-- 警情情况(右侧) -->
|
||||
<div class="alarm-section">
|
||||
<div class="alarm-title">所属项目</div>
|
||||
<div class="alarm-card special">
|
||||
<div class="project-info">
|
||||
<div class="project-icon">
|
||||
<el-icon><OfficeBuilding /></el-icon>
|
||||
</div>
|
||||
<div class="project-detail">
|
||||
<div class="project-name">{{ currentProject.name }}</div>
|
||||
<div class="project-address">
|
||||
<el-icon><Location /></el-icon>
|
||||
<span>{{ currentProject.address }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button class="change-project-btn" type="primary" link @click="openChangeProjectDialog"
|
||||
>切换项目</el-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="alarm-title">警情情况</div>
|
||||
|
||||
<div class="alarm-card">
|
||||
<div class="alarm-row">
|
||||
<div class="alarm-icon device-icon">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
</div>
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-labels">
|
||||
<span class="label-item">设备总数</span>
|
||||
<span class="label-item">在线设备数</span>
|
||||
<span class="label-item">离线设备数</span>
|
||||
</div>
|
||||
<div class="alarm-values">
|
||||
<span class="value-item">{{ alarmSituationSummary.device.total }}</span>
|
||||
<span class="value-item online">{{ alarmSituationSummary.device.online }}</span>
|
||||
<span class="value-item offline">{{ alarmSituationSummary.device.offline }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="record-tag">智能竖井</div>
|
||||
<el-table :data="alarmTableData" border stripe class="alarm-table">
|
||||
<el-table-column align="center" label="序号" prop="index" width="60" />
|
||||
<el-table-column align="left" label="事件ID" prop="eventId" width="110" />
|
||||
<el-table-column align="left" label="设备别名" prop="deviceAlias" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column align="left" label="设备号" prop="deviceNum" width="150" />
|
||||
<el-table-column align="center" label="线路" prop="line" width="80" />
|
||||
<el-table-column align="left" label="报警/预警类型" prop="alarmType" width="120" />
|
||||
<el-table-column align="center" label="报警/预警值(红色)/ 阈值(蓝色)" width="200">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.alarmValue" class="value-red">{{ scope.row.alarmValue }}</span>
|
||||
<span v-if="scope.row.alarmValue && scope.row.threshold"> / </span>
|
||||
<span v-if="scope.row.threshold" class="value-blue">{{ scope.row.threshold }}</span>
|
||||
<span v-if="!scope.row.alarmValue && !scope.row.threshold">/</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="耗时" prop="duration" width="80" />
|
||||
<el-table-column align="center" label="状态" prop="status" width="90">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === '已处理' ? 'success' : 'danger'" size="small" effect="light">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="报警/预警时间" prop="alarmTime" width="170" />
|
||||
<el-table-column align="center" label="动作" width="100" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button v-if="scope.row.status === '未处理'" type="primary" link @click="handleProcess(scope.row)">
|
||||
处理
|
||||
</el-button>
|
||||
<el-button v-else type="primary" link @click="handleRecall(scope.row)">撤回</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="alarm-card">
|
||||
<div class="alarm-row">
|
||||
<div class="alarm-icon line-icon">
|
||||
<el-icon><Connection /></el-icon>
|
||||
</div>
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-labels">
|
||||
<span class="label-item">线路总数</span>
|
||||
<span class="label-item">在线线路数</span>
|
||||
<span class="label-item">离线线路数</span>
|
||||
</div>
|
||||
<div class="alarm-values">
|
||||
<span class="value-item">{{ alarmSituationSummary.line.total }}</span>
|
||||
<span class="value-item online">{{ alarmSituationSummary.line.online }}</span>
|
||||
<span class="value-item offline">{{ alarmSituationSummary.line.offline }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alarm-card">
|
||||
<div class="alarm-row">
|
||||
<div class="alarm-icon warn-icon">
|
||||
<el-icon><Bell /></el-icon>
|
||||
</div>
|
||||
<div class="alarm-content alarm-stats-grid">
|
||||
<div class="alarm-stat">
|
||||
<span class="alarm-stat-label">本月报警总数</span>
|
||||
<span class="alarm-stat-value danger">{{ alarmSituationSummary.alarm.currentMonthTotal }}</span>
|
||||
</div>
|
||||
<div class="alarm-stat">
|
||||
<span class="alarm-stat-label">昨日报警总数</span>
|
||||
<span class="alarm-stat-value danger">{{ alarmSituationSummary.alarm.yesterdayTotal }}</span>
|
||||
</div>
|
||||
<div class="alarm-stat">
|
||||
<span class="alarm-stat-label">今日报警总数</span>
|
||||
<span class="alarm-stat-value danger">{{ alarmSituationSummary.alarm.todayTotal }}</span>
|
||||
</div>
|
||||
<div class="alarm-stat">
|
||||
<span class="alarm-stat-label">今日报警设备数</span>
|
||||
<span class="alarm-stat-value danger">{{ alarmSituationSummary.alarm.todayDeviceCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="alarm-card">
|
||||
<div class="alarm-row process-row">
|
||||
<div class="process-stat">
|
||||
<div class="process-ring green">
|
||||
<span class="process-ring-value">0.18%</span>
|
||||
</div>
|
||||
<div class="process-label">本月已处理报警数</div>
|
||||
<div class="process-sub-value processed">35</div>
|
||||
</div>
|
||||
<div class="process-divider"></div>
|
||||
<div class="process-stat">
|
||||
<div class="process-ring red">
|
||||
<span class="process-ring-value">99.82%</span>
|
||||
</div>
|
||||
<div class="process-label">本月未处理报警数</div>
|
||||
<div class="process-sub-value unprocessed">19606</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 警情情况(右侧) -->
|
||||
<div class="alarm-section">
|
||||
<div class="alarm-title">警情情况</div>
|
||||
|
||||
<div class="alarm-card">
|
||||
<div class="alarm-row">
|
||||
<div class="alarm-icon device-icon">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
</div>
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-labels">
|
||||
<span class="label-item">设备总数</span>
|
||||
<span class="label-item">在线设备数</span>
|
||||
<span class="label-item">离线设备数</span>
|
||||
</div>
|
||||
<div class="alarm-values">
|
||||
<span class="value-item">17</span>
|
||||
<span class="value-item online">12</span>
|
||||
<span class="value-item offline">5</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 警情列表 -->
|
||||
<div class="alarm-list">
|
||||
<div class="list-header">
|
||||
<div class="list-title">
|
||||
<span class="dot"></span>
|
||||
警情列表
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alarm-card">
|
||||
<div class="alarm-row">
|
||||
<div class="alarm-icon line-icon">
|
||||
<el-icon><Connection /></el-icon>
|
||||
</div>
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-labels">
|
||||
<span class="label-item">线路总数</span>
|
||||
<span class="label-item">在线线路数</span>
|
||||
<span class="label-item">离线线路数</span>
|
||||
</div>
|
||||
<div class="alarm-values">
|
||||
<span class="value-item">92</span>
|
||||
<span class="value-item online">74</span>
|
||||
<span class="value-item offline">18</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alarm-card">
|
||||
<div class="alarm-row">
|
||||
<div class="alarm-icon warn-icon">
|
||||
<el-icon><Bell /></el-icon>
|
||||
</div>
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-labels">
|
||||
<span class="label-item">本月报警总数</span>
|
||||
<span class="label-item">本月预警总数</span>
|
||||
<span class="label-item">昨日报警总数</span>
|
||||
</div>
|
||||
<div class="alarm-values">
|
||||
<span class="value-item danger">19641</span>
|
||||
<span class="value-item warning">208</span>
|
||||
<span class="value-item danger">25</span>
|
||||
</div>
|
||||
<div class="alarm-labels second-row">
|
||||
<span class="label-item">今日报警总数</span>
|
||||
<span class="label-item">今日报警设备数</span>
|
||||
</div>
|
||||
<div class="alarm-values two-cols">
|
||||
<span class="value-item danger">13</span>
|
||||
<span class="value-item danger">5</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alarm-card">
|
||||
<div class="alarm-row process-row">
|
||||
<div class="process-stat">
|
||||
<div class="process-ring green">
|
||||
<span class="process-ring-value">0.18%</span>
|
||||
</div>
|
||||
<div class="process-label">本月已处理报警数</div>
|
||||
<div class="process-sub-value processed">35</div>
|
||||
</div>
|
||||
<div class="process-divider"></div>
|
||||
<div class="process-stat">
|
||||
<div class="process-ring red">
|
||||
<span class="process-ring-value">99.82%</span>
|
||||
</div>
|
||||
<div class="process-label">本月未处理报警数</div>
|
||||
<div class="process-sub-value unprocessed">19606</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="alarmTableData" border stripe class="alarm-table">
|
||||
<el-table-column align="center" label="序号" prop="index" width="60" />
|
||||
<el-table-column align="left" label="事件ID" prop="eventId" width="110" />
|
||||
<el-table-column align="left" label="设备别名" prop="deviceAlias" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column align="left" label="设备号" prop="deviceNum" width="150" />
|
||||
<el-table-column align="center" label="线路" prop="line" width="80" />
|
||||
<el-table-column align="left" label="报警/预警类型" prop="alarmType" width="120" />
|
||||
<el-table-column align="center" label="报警/预警值(红色)/ 阈值(蓝色)" width="200">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.alarmValue" class="value-red">{{ scope.row.alarmValue }}</span>
|
||||
<span v-if="scope.row.alarmValue && scope.row.threshold"> / </span>
|
||||
<span v-if="scope.row.threshold" class="value-blue">{{ scope.row.threshold }}</span>
|
||||
<span v-if="!scope.row.alarmValue && !scope.row.threshold">/</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="耗时" prop="duration" width="80" />
|
||||
<el-table-column align="center" label="状态" prop="status" width="90">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === '已处理' ? 'success' : 'danger'" size="small" effect="light">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="报警/预警时间" prop="alarmTime" width="170" />
|
||||
<el-table-column align="center" label="动作" width="100" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button v-if="scope.row.status === '未处理'" type="primary" link @click="handleProcess(scope.row)">
|
||||
处理
|
||||
</el-button>
|
||||
<el-button v-else type="primary" link @click="handleRecall(scope.row)">撤回</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="projectDialogVisible" destroy-on-close title="更换项目" width="480px">
|
||||
<el-radio-group v-model="selectedProjectId" class="project-radio-group">
|
||||
<el-radio v-for="item in projectList" :key="item.id" :value="item.id" class="project-radio-item">
|
||||
<div class="project-option">
|
||||
<div class="project-option-name">{{ item.name }}</div>
|
||||
<div class="project-option-address">{{ item.address }}</div>
|
||||
</div>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<template #footer>
|
||||
<el-button @click="projectDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmChangeProject">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import * as serve from '@/api/masterStation/project'
|
||||
import * as equipmentServe from '@/api/masterStation/equipment'
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { FullScreen, Monitor, Connection, Bell } from '@element-plus/icons-vue'
|
||||
import { FullScreen, Monitor, Connection, Bell, OfficeBuilding, Location } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'MasterStationProject'
|
||||
})
|
||||
|
||||
const PROJECT_STORAGE_KEY = 'masterStation_current_project_id'
|
||||
const MAP_ZOOM = 13
|
||||
const MAP_ZOOM_OUT = 9
|
||||
const MAP_STEP_DURATION = 350
|
||||
|
||||
const normalizeProject = (item) => ({
|
||||
id: item.projectId,
|
||||
name: item.projectName,
|
||||
address: item.projectAddress,
|
||||
lng: Number(item.projectLong),
|
||||
lat: Number(item.projectLat),
|
||||
projectUser: item.projectUser,
|
||||
projectPhone: item.projectPhone
|
||||
})
|
||||
|
||||
const projectList = ref([])
|
||||
const currentProject = ref({ id: null, name: '', address: '', lng: 119.1, lat: 36.7 })
|
||||
const projectDialogVisible = ref(false)
|
||||
const selectedProjectId = ref(null)
|
||||
const alarmSituationSummary = ref({
|
||||
device: { total: 0, online: 0, offline: 0 },
|
||||
line: { total: 0, online: 0, offline: 0 },
|
||||
alarm: { currentMonthTotal: 0, yesterdayTotal: 0, todayTotal: 0, todayDeviceCount: 0 },
|
||||
preAlarm: { currentMonthTotal: 0 }
|
||||
})
|
||||
|
||||
let mapSwitchToken = 0
|
||||
|
||||
const waitMapEvent = (map, eventName, token) => {
|
||||
return new Promise((resolve) => {
|
||||
const timer = setTimeout(() => {
|
||||
map.off(eventName, handler)
|
||||
resolve()
|
||||
}, MAP_STEP_DURATION + 400)
|
||||
const handler = () => {
|
||||
clearTimeout(timer)
|
||||
map.off(eventName, handler)
|
||||
if (token === mapSwitchToken) resolve()
|
||||
}
|
||||
map.on(eventName, handler)
|
||||
})
|
||||
}
|
||||
|
||||
const moveMapToProject = (project) => {
|
||||
if (!mapInstance.value || !project?.lng || !project?.lat) return
|
||||
mapInstance.value.setZoomAndCenter(MAP_ZOOM, [project.lng, project.lat], true)
|
||||
}
|
||||
|
||||
const animateMapSwitch = async (fromProject, toProject) => {
|
||||
const map = mapInstance.value
|
||||
if (!map || !fromProject?.lng || !fromProject?.lat || !toProject?.lng || !toProject?.lat) return
|
||||
|
||||
const token = ++mapSwitchToken
|
||||
const fromCenter = [fromProject.lng, fromProject.lat]
|
||||
const toCenter = [toProject.lng, toProject.lat]
|
||||
|
||||
map.setZoomAndCenter(MAP_ZOOM, fromCenter, true)
|
||||
|
||||
map.setZoom(MAP_ZOOM_OUT, false, MAP_STEP_DURATION)
|
||||
await waitMapEvent(map, 'zoomend', token)
|
||||
if (token !== mapSwitchToken) return
|
||||
|
||||
hideProjectMarker()
|
||||
map.panTo(toCenter, MAP_STEP_DURATION)
|
||||
await waitMapEvent(map, 'moveend', token)
|
||||
if (token !== mapSwitchToken) return
|
||||
|
||||
map.setZoom(MAP_ZOOM, false, MAP_STEP_DURATION)
|
||||
await waitMapEvent(map, 'zoomend', token)
|
||||
if (token !== mapSwitchToken) return
|
||||
|
||||
updateProjectMarker(toProject)
|
||||
}
|
||||
|
||||
const applyCurrentProject = (project) => {
|
||||
if (!project) return
|
||||
currentProject.value = project
|
||||
selectedProjectId.value = project.id
|
||||
updateProjectMarker(project)
|
||||
moveMapToProject(project)
|
||||
fetchAlarmSituationSummary()
|
||||
}
|
||||
|
||||
const fetchAlarmSituationSummary = () => {
|
||||
if (!currentProject.value.id) return
|
||||
serve.getAlarmSituationSummary({ projectId: currentProject.value.id }).then((res) => {
|
||||
if (res.code !== 0 || !res.data) return
|
||||
alarmSituationSummary.value = {
|
||||
device: { total: 0, online: 0, offline: 0, ...res.data.device },
|
||||
line: { total: 0, online: 0, offline: 0, ...res.data.line },
|
||||
alarm: {
|
||||
currentMonthTotal: 0,
|
||||
yesterdayTotal: 0,
|
||||
todayTotal: 0,
|
||||
todayDeviceCount: 0,
|
||||
...res.data.alarm
|
||||
},
|
||||
preAlarm: { currentMonthTotal: 0, ...res.data.preAlarm }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getProjectList()
|
||||
getAlarmRecordListByPage()
|
||||
nextTick(() => {
|
||||
initMap()
|
||||
if (mapWrapperRef.value) {
|
||||
mapResizeObserver = new ResizeObserver(handleResize)
|
||||
mapResizeObserver.observe(mapWrapperRef.value)
|
||||
}
|
||||
})
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
// 获取警情情况
|
||||
const getAlarmRecordListByPage = () => {
|
||||
console.log('currentProject.value', currentProject.value)
|
||||
if (!currentProject.value.id) return
|
||||
equipmentServe
|
||||
.getAlarmRecordListByPage({ page: 1, pageSize: 10, projectId: currentProject.value.id })
|
||||
.then((res) => {
|
||||
if (res.code !== 0 || !res.data) return
|
||||
alarmTableData.value = res.data.list
|
||||
})
|
||||
}
|
||||
// 获取项目列表
|
||||
const getProjectList = () => {
|
||||
return serve.getProjectList().then((res) => {
|
||||
if (res.code !== 0 || !res.data?.length) return
|
||||
|
||||
const seen = new Set()
|
||||
projectList.value = res.data
|
||||
.filter((item) => {
|
||||
if (seen.has(item.projectId)) return false
|
||||
seen.add(item.projectId)
|
||||
return true
|
||||
})
|
||||
.map(normalizeProject)
|
||||
|
||||
const savedId = Number(localStorage.getItem(PROJECT_STORAGE_KEY))
|
||||
// console.log('savedId', savedId)
|
||||
const project = projectList.value.find((item) => item.id === savedId) || projectList.value[0]
|
||||
// console.log('project', project)
|
||||
applyCurrentProject(project)
|
||||
})
|
||||
}
|
||||
|
||||
const openChangeProjectDialog = () => {
|
||||
selectedProjectId.value = currentProject.value.id
|
||||
projectDialogVisible.value = true
|
||||
}
|
||||
|
||||
const confirmChangeProject = () => {
|
||||
const project = projectList.value.find((item) => item.id === selectedProjectId.value)
|
||||
if (!project) return
|
||||
|
||||
if (project.id === currentProject.value.id) {
|
||||
projectDialogVisible.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const prevProject = { ...currentProject.value }
|
||||
currentProject.value = project
|
||||
selectedProjectId.value = project.id
|
||||
localStorage.setItem(PROJECT_STORAGE_KEY, String(project.id))
|
||||
projectDialogVisible.value = false
|
||||
ElMessage.success(`已切换至:${project.name}`)
|
||||
fetchAlarmSituationSummary()
|
||||
nextTick(() => {
|
||||
animateMapSwitch(prevProject, project)
|
||||
})
|
||||
}
|
||||
|
||||
const deviceTab = ref('device')
|
||||
const mapWrapperRef = ref(null)
|
||||
const mapInstance = ref(null)
|
||||
const markers = ref([])
|
||||
let projectMarker = null
|
||||
let mapResizeObserver = null
|
||||
let resizeTimer = null
|
||||
|
||||
const alarmTableData = ref([
|
||||
{
|
||||
@@ -237,33 +447,48 @@
|
||||
}
|
||||
])
|
||||
|
||||
const mapDevices = [
|
||||
{ lng: 116.4, lat: 39.9, label: '10', type: 'cluster' },
|
||||
{ lng: 113.2, lat: 23.1, label: '6', type: 'cluster' },
|
||||
{ lng: 118.8, lat: 32.0, label: '98CC4D2052AE', subLabel: '98CC4D2052AE', type: 'device' }
|
||||
]
|
||||
|
||||
const createClusterContent = (label) => {
|
||||
return `
|
||||
<div class="map-cluster-marker">
|
||||
<div class="map-cluster-pulse"></div>
|
||||
<div class="map-cluster-core">${label}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
const createDeviceContent = (label, subLabel) => {
|
||||
const createProjectMarkerContent = (name) => {
|
||||
return `
|
||||
<div class="map-device-marker">
|
||||
<div class="map-device-icon">📍</div>
|
||||
<div class="map-device-label">
|
||||
<div class="map-device-name">${label}</div>
|
||||
<div class="map-device-sub">[${subLabel}]</div>
|
||||
<div class="map-device-name">${name}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
const hideProjectMarker = () => {
|
||||
if (!projectMarker || !mapInstance.value) return
|
||||
mapInstance.value.remove(projectMarker)
|
||||
projectMarker = null
|
||||
}
|
||||
|
||||
const updateProjectMarker = (project) => {
|
||||
if (!mapInstance.value || !project?.lng || !project?.lat) return
|
||||
const position = [project.lng, project.lat]
|
||||
|
||||
if (!projectMarker) {
|
||||
projectMarker = new AMap.Marker({
|
||||
position,
|
||||
content: createProjectMarkerContent(project.name),
|
||||
offset: new AMap.Pixel(-10, -30),
|
||||
zIndex: 10
|
||||
})
|
||||
mapInstance.value.add(projectMarker)
|
||||
return
|
||||
}
|
||||
|
||||
projectMarker.setPosition(position)
|
||||
const content = projectMarker.getContent()
|
||||
if (content?.querySelector) {
|
||||
const nameEl = content.querySelector('.map-device-name')
|
||||
if (nameEl) nameEl.textContent = project.name
|
||||
} else {
|
||||
projectMarker.setContent(createProjectMarkerContent(project.name))
|
||||
}
|
||||
}
|
||||
|
||||
const initMap = () => {
|
||||
if (typeof AMap === 'undefined') {
|
||||
console.error('高德地图API未加载')
|
||||
@@ -271,29 +496,16 @@
|
||||
}
|
||||
|
||||
mapInstance.value = new AMap.Map('projectMap', {
|
||||
zoom: 5,
|
||||
center: [116.4074, 39.9042],
|
||||
zoom: MAP_ZOOM,
|
||||
center: [currentProject.value.lng, currentProject.value.lat],
|
||||
mapStyle: 'amap://styles/whitesmoke',
|
||||
viewMode: '2D',
|
||||
features: ['bg', 'road', 'building', 'point']
|
||||
})
|
||||
|
||||
mapDevices.forEach((item) => {
|
||||
const content =
|
||||
item.type === 'cluster'
|
||||
? createClusterContent(item.label)
|
||||
: createDeviceContent(item.label, item.subLabel)
|
||||
|
||||
const marker = new AMap.Marker({
|
||||
position: [item.lng, item.lat],
|
||||
content,
|
||||
offset: item.type === 'cluster' ? new AMap.Pixel(-18, -18) : new AMap.Pixel(-10, -30),
|
||||
zIndex: item.type === 'device' ? 10 : 5
|
||||
})
|
||||
|
||||
mapInstance.value.add(marker)
|
||||
markers.value.push(marker)
|
||||
})
|
||||
if (currentProject.value.id) {
|
||||
updateProjectMarker(currentProject.value)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMapFullscreen = () => {
|
||||
@@ -317,18 +529,21 @@
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
mapInstance.value?.resize()
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = setTimeout(() => {
|
||||
mapInstance.value?.resize()
|
||||
}, 150)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initMap()
|
||||
})
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(resizeTimer)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
mapResizeObserver?.disconnect()
|
||||
mapResizeObserver = null
|
||||
if (projectMarker) {
|
||||
mapInstance.value?.remove(projectMarker)
|
||||
projectMarker = null
|
||||
}
|
||||
if (mapInstance.value) {
|
||||
mapInstance.value.destroy()
|
||||
mapInstance.value = null
|
||||
@@ -341,17 +556,18 @@
|
||||
background-color: #f5f7fa;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-height: calc(100vh - 120px);
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
flex: 1;
|
||||
.top-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.overview-card,
|
||||
@@ -364,7 +580,12 @@
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@@ -419,7 +640,8 @@
|
||||
}
|
||||
|
||||
.map-container {
|
||||
height: 360px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #ebeef5;
|
||||
@@ -449,6 +671,103 @@
|
||||
|
||||
.alarm-card {
|
||||
padding: 14px 12px;
|
||||
|
||||
&.special {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.project-info {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.project-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #409eff, #2777ec);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.project-detail {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 6px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.project-address {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
|
||||
.el-icon {
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.change-project-btn {
|
||||
align-self: flex-end;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.project-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.project-radio-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
height: auto;
|
||||
margin-right: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
|
||||
&.is-checked {
|
||||
border-color: #409eff;
|
||||
background: #ecf5ff;
|
||||
}
|
||||
}
|
||||
|
||||
.project-option {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.project-option-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.project-option-address {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.alarm-row {
|
||||
@@ -487,6 +806,41 @@
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
|
||||
&.alarm-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.alarm-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.alarm-stat-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.alarm-stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
font-family: 'DIN', 'Arial', sans-serif;
|
||||
color: #303133;
|
||||
line-height: 1.2;
|
||||
|
||||
&.danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: #e6a23c;
|
||||
}
|
||||
}
|
||||
|
||||
.alarm-labels {
|
||||
@@ -650,18 +1004,6 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.record-tag {
|
||||
display: inline-block;
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
padding: 5px 14px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
margin-bottom: -1px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.alarm-table {
|
||||
width: 100%;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user