| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894 |
- <!--窨井盖监测首页大屏-->
- <template>
- <div class="monitor-container">
- <!-- 左侧栏 -->
- <div class="left-col">
- <!-- 待办工单 -->
- <div class="panel">
- <h3 class="panel-title">待办工单 <span class="title-count">{{ workOrders.length }} 条</span></h3>
- <el-table :data="workOrders" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.work">
- <el-table-column prop="orderNo" label="工单编号" min-width="120" show-overflow-tooltip />
- <el-table-column prop="deviceCode" label="设备" min-width="100" show-overflow-tooltip />
- <el-table-column label="优先级" width="70">
- <template #default="{ row }">
- <span :class="['priority-tag', priorityClass(row.orderLevel)]">{{ priorityText(row.orderLevel) }}</span>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <!-- 告警类型统计(15天内) -->
- <div class="panel">
- <h3 class="panel-title">
- 告警类型统计(15天内)
- <span class="title-count">{{ alarmRangeText }}</span>
- </h3>
- <div ref="alarmChartRef" class="chart" v-loading="loading.alarm" />
- </div>
- <!-- 设备区域信息 -->
- <div class="panel">
- <h3 class="panel-title">设备区域信息</h3>
- <div ref="areaChartRef" class="chart" v-loading="loading.device" />
- </div>
- </div>
- <!-- 中间地图区域 -->
- <div class="center-col">
- <div class="panel map-panel">
- <div class="map-header">
- <span>窨井设备分布</span>
- <div class="map-legend">
- <span><i class="legend-dot online"></i> 在线 {{ stats.online }}</span>
- <span><i class="legend-dot alert"></i> 告警 {{ stats.alarm }}</span>
- <span><i class="legend-dot offline"></i> 离线 {{ stats.offline }}</span>
- </div>
- </div>
- <div id="homeMap" class="bmap-container"></div>
- </div>
- <!-- 设备分类 -->
- <div class="panel chart-panel">
- <h3 class="panel-title">设备分类</h3>
- <div ref="deviceTypeChartRef" class="chart" v-loading="loading.device" />
- </div>
- </div>
- <!-- 右侧栏 -->
- <div class="right-col">
- <!-- 设备列表 -->
- <div class="panel">
- <h3 class="panel-title">设备列表 <span class="title-count">共 {{ filteredDevices.length }} 台</span></h3>
- <div class="filter-bar">
- <el-select v-model="deviceTypeFilter" size="small" placeholder="设备类型" clearable style="width: 120px">
- <el-option v-for="item in deviceTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
- </el-select>
- </div>
- <el-table :data="filteredDevices" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.device">
- <el-table-column prop="name" label="设备名称" min-width="100" show-overflow-tooltip />
- <el-table-column label="类型" width="70">
- <template #default="{ row }">{{ typeText(row.typeId, row.typeName) }}</template>
- </el-table-column>
- <el-table-column label="电量" width="60">
- <template #default="{ row }">
- <el-tag :type="batteryTag(row.battery)" size="small">{{ row.battery ?? '-' }}%</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="状态" width="60">
- <template #default="{ row }">
- <span :class="['status-dot', row.status]"></span>
- <span class="status-text">{{ statusText(row.status) }}</span>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <!-- 动态信息 -->
- <div class="panel">
- <h3 class="panel-title">动态信息</h3>
- <el-table :data="dynamicLogs" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.alarm">
- <el-table-column prop="time" label="时间" width="140" />
- <el-table-column prop="event" label="事件" min-width="120" show-overflow-tooltip />
- <el-table-column label="状态" width="70">
- <template #default="{ row }">
- <span :class="['status-tag', row.alarmStatus === 1 ? 'done' : 'processing']">{{ row.alarmStatus === 1 ? '已处理' : '未处理' }}</span>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, reactive, onMounted, onBeforeUnmount, nextTick, computed } from 'vue'
- import * as echarts from 'echarts'
- import { getManholeLayerData } from '@/api/pipeNetwork/basic'
- import { listWorkOrder } from '@/api/pipeNetwork/ops/workOrder.js'
- import { ORDER_STATUS_GROUP } from '@/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js'
- import { getAlarmDataPage } from '@/api/drainage'
- import { ElMessage } from 'element-plus'
- import locationIcon from '@/assets/images/location.png'
- // 台账存 WGS84(设备表单选点经 bd09ToWgs84 回填),百度地图必须转 BD-09 再画
- import { convertCoord } from '@/utils/coordTransform'
- // ==================== 状态 ====================
- const loading = reactive({ work: false, alarm: false, device: false })
- const stats = reactive({ total: 0, online: 0, offline: 0, alarm: 0 })
- const workOrders = ref([])
- const allDevices = ref([])
- const dynamicLogs = ref([])
- const deviceTypeFilter = ref('')
- const alarmRangeText = ref(formatAlarmRange())
- /** 告警统计区间文案,空数据时也展示,避免面板看不出统计范围 */
- function formatAlarmRange() {
- const end = new Date()
- const start = new Date(end.getTime() - ALARM_RANGE_DAYS * 24 * 3600 * 1000)
- const label = date => `${date.getMonth() + 1}-${String(date.getDate()).padStart(2, '0')}`
- return `${label(start)} ~ ${label(end)}`
- }
- // 图表引用
- const alarmChartRef = ref(null)
- const areaChartRef = ref(null)
- const deviceTypeChartRef = ref(null)
- let alarmChart = null, areaChart = null, deviceTypeChart = null
- // ==================== 常量映射 ====================
- /** 历史遗留窨井类型编码,仅用于后端未返回 typeName 时兜底展示 */
- const LEGACY_TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
- // 所属片区:后端 DistrictEnum(0-城南 1-城北 2-太常)
- const DISTRICT_MAP = { 0: '城南', 1: '城北', 2: '太常' }
- function districtLabel(code) {
- return code == null ? '' : (DISTRICT_MAP[code] || String(code))
- }
- const tableHeaderStyle = { background: 'rgba(0,100,200,0.3)', color: '#fff', borderColor: '#2b4b8c' }
- const priorityText = (l) => ({ 0: '紧急', 1: '一般', 2: '低' }[l] || '-')
- const priorityClass = (l) => ({ 0: 'high', 1: 'medium', 2: 'low' }[l] || 'low')
- /** 待办工单口径:待派单/待接单/已接单/处理中/延期中(枚举派生,见 workOrderDict.js) */
- const TODO_ORDER_STATUS = ORDER_STATUS_GROUP.todo.join(',')
- /** 设备父类别ID(equipment_type.parent_id),与工单列表页保持同一口径 */
- const DEVICE_TYPE_PARENT_ID = '4'
- /** 窨井设备大类根ID(EquipmentTypeEnum.ROOT_MANHOLE_DEVICE),报警查询按大类隔离 */
- const MANHOLE_ROOT_TYPE_ID = 4
- /** 报警查询后端限制 pageSize 不超过 100 */
- const ALARM_PAGE_SIZE = 100
- const ALARM_MAX_PAGES = 3
- /** 告警统计窗口(天) */
- const ALARM_RANGE_DAYS = 15
- /** 无设备数据时仍展示的区域维度,取自 DistrictEnum(0-城南 1-城北 2-太常) */
- const DEFAULT_DISTRICTS = ['城南', '城北', '太常']
- const typeText = (id, name) => name || LEGACY_TYPE_MAP[id] || (id ? `类型${id}` : '-')
- const statusText = (s) => ({ normal: '在线', alert: '告警', offline: '离线' }[s] || s)
- const batteryTag = (b) => {
- if (b == null) return 'info'
- if (Number(b) <= 20) return 'danger'
- if (Number(b) <= 50) return 'warning'
- return 'success'
- }
- // ==================== 筛选后设备列表 ====================
- /** 设备类型下拉项:跟着台账实际类型走,不再写死 5/9/10 */
- const deviceTypeOptions = computed(() => {
- const map = new Map()
- allDevices.value.forEach(d => {
- const value = String(d.typeId ?? '')
- if (!value || map.has(value)) return
- map.set(value, { value, label: typeText(d.typeId, d.typeName) })
- })
- return Array.from(map.values())
- })
- const filteredDevices = computed(() => {
- if (!deviceTypeFilter.value) return allDevices.value
- return allDevices.value.filter(d => String(d.typeId) === deviceTypeFilter.value)
- })
- // ==================== 数据加载 ====================
- async function loadAll() {
- await Promise.allSettled([loadDevices(), loadWorkOrders(), loadAlarmData()])
- await nextTick()
- handleResize()
- }
- async function loadDevices() {
- loading.device = true
- try {
- const res = await getManholeLayerData({ isQueryManholeData: true })
- const devices = res.data?.devices || []
- allDevices.value = devices.map(d => {
- const onlineStatus = Number(d.equipmentStatus?.onlineStatus)
- const alarmStatus = Number(d.equipmentStatus?.alarmStatus)
- const manholeAlarm = Number(d.manholeData?.alarmStatus)
- const isAlert = alarmStatus === 1 || manholeAlarm === 1 ||
- Number(d.manholeData?.waterInfiltrationAlarmStatus) === 1 ||
- Number(d.manholeData?.waterLevelAlarmStatus) === 1
- let status = 'offline'
- if (isAlert) status = 'alert'
- else if (onlineStatus === 1) status = 'normal'
- return {
- id: d.equipmentId, code: d.equipmentCode, name: d.equipmentName,
- typeId: d.equipmentTypeId, typeName: d.equipmentTypeName,
- location: d.equipmentLocation, district: districtLabel(d.district),
- lng: d.longitude != null ? Number(d.longitude) : null,
- lat: d.latitude != null ? Number(d.latitude) : null,
- battery: d.manholeData?.batteryLevel ?? null,
- status
- }
- })
- const statistics = res.data?.statistics || {}
- stats.total = statistics.total || allDevices.value.length
- stats.online = statistics.online || 0
- stats.offline = statistics.offline || 0
- stats.alarm = statistics.alarm || 0
- renderAreaChart()
- renderDeviceTypeChart()
- refreshMarkers()
- } catch (e) {
- ElMessage.error('设备数据加载失败')
- // 加载失败也铺出空态骨架,面板不留白
- renderAreaChart()
- renderDeviceTypeChart()
- } finally {
- loading.device = false
- }
- }
- async function loadWorkOrders() {
- loading.work = true
- try {
- const res = await listWorkOrder({
- pageNum: 1,
- pageSize: 20,
- orderStatus: TODO_ORDER_STATUS,
- deviceTypeParentId: DEVICE_TYPE_PARENT_ID
- })
- workOrders.value = res.data?.records || []
- } catch (e) {
- ElMessage.error('工单数据加载失败')
- } finally {
- loading.work = false
- }
- }
- function formatDateTime(date) {
- const pad = value => String(value).padStart(2, '0')
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
- }
- /** 报警类型展示:优先报警内容,其次报警编码 */
- function alarmTypeText(record) {
- return record.alarmContent || record.warningCode || '其他告警'
- }
- /**
- * 拉取近 15 天的窨井报警
- *
- * rootTypeId 是报警查询的必填项(后端校验「设备大类不能为空」),
- * pageSize 上限 100,因此按页累加,最多取 3 页用于首页统计。
- */
- async function fetchRecentAlarms() {
- const params = {
- rootTypeId: MANHOLE_ROOT_TYPE_ID,
- startTime: formatDateTime(new Date(Date.now() - 15 * 24 * 3600 * 1000))
- }
- const records = []
- for (let page = 1; page <= ALARM_MAX_PAGES; page++) {
- const res = await getAlarmDataPage(page, ALARM_PAGE_SIZE, params)
- const pageRecords = res.rows || res.data?.records || res.data || []
- records.push(...pageRecords)
- if (pageRecords.length < ALARM_PAGE_SIZE) break
- }
- return records
- }
- async function loadAlarmData() {
- loading.alarm = true
- alarmRangeText.value = formatAlarmRange()
- try {
- const records = await fetchRecentAlarms()
- const fifteenDaysAgo = new Date(Date.now() - 15 * 24 * 3600 * 1000)
- const recentAlarms = records.filter(r => !r.createTime || new Date(String(r.createTime).replace(/-/g, '/')) >= fifteenDaysAgo)
- const typeMap = {}
- recentAlarms.forEach(r => {
- const type = alarmTypeText(r)
- typeMap[type] = (typeMap[type] || 0) + 1
- })
- renderAlarmChart(typeMap)
- // 动态信息(最新10条)
- dynamicLogs.value = records.slice(0, 10).map(r => ({
- time: r.createTime || '',
- event: `${alarmTypeText(r)} - ${r.deviceName || r.deviceCode || ''}`,
- alarmStatus: r.alarmStatus ?? 0
- }))
- } catch (e) {
- console.error(e)
- ElMessage.error('告警数据加载失败')
- // 加载失败也铺出空态骨架(含统计区间),面板不留白
- renderAlarmChart({})
- } finally {
- loading.alarm = false
- }
- }
- // ==================== ECharts ====================
- function renderAlarmChart(typeMap) {
- if (!alarmChartRef.value) return
- if (!alarmChart) alarmChart = echarts.init(alarmChartRef.value)
- const colors = ['#ff3333', '#ffb333', '#33cc33', '#3399ff', '#9966ff', '#ff6699', '#33cccc']
- const data = Object.entries(typeMap || {}).map(([name, value], i) => ({
- name, value, itemStyle: { color: colors[i % colors.length] }
- }))
- const hasData = data.length > 0
- alarmChart.setOption({
- backgroundColor: 'transparent',
- // 无告警时保留统计区间与说明,避免面板看起来是空的
- title: hasData ? { show: false } : {
- text: '近 15 天暂无告警',
- subtext: `统计区间 ${alarmRangeText.value}`,
- left: 'center',
- top: 'middle',
- textStyle: { color: '#9ba8c0', fontSize: 12, fontWeight: 'normal' },
- subtextStyle: { color: '#6f7d95', fontSize: 11 }
- },
- tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
- legend: { bottom: 0, textStyle: { color: '#ccc', fontSize: 10 }, show: hasData },
- // 空数据时不挂饼图系列,否则会残留一个灰色的空圆环
- series: hasData ? [{
- type: 'pie', radius: ['35%', '60%'], center: ['50%', '40%'],
- label: { color: '#ccc', fontSize: 10 },
- data
- }] : []
- }, true)
- }
- function renderAreaChart() {
- if (!areaChartRef.value) return
- if (!areaChart) areaChart = echarts.init(areaChartRef.value)
- const areaMap = {}
- allDevices.value.forEach(d => {
- const area = d.district || d.location || '未分配区域'
- // 取区域名(截取前4个字)
- const shortArea = area.length > 6 ? area.slice(0, 6) + '...' : area
- areaMap[shortArea] = (areaMap[shortArea] || 0) + 1
- })
- let entries = Object.entries(areaMap).sort((a, b) => b[1] - a[1]).slice(0, 8)
- const hasData = entries.length > 0
- // 无设备时仍按默认区域维度铺出骨架,面板不会空成一片
- if (!hasData) {
- entries = DEFAULT_DISTRICTS.map(name => [name, 0])
- }
- areaChart.setOption({
- backgroundColor: 'transparent',
- title: hasData ? { show: false } : {
- text: '暂无设备,按默认区域维度展示',
- left: 'center',
- top: 0,
- textStyle: { color: '#6f7d95', fontSize: 11, fontWeight: 'normal' }
- },
- tooltip: { trigger: 'axis' },
- grid: { left: '12%', right: '8%', bottom: '18%', top: '18%' },
- xAxis: {
- type: 'category', data: entries.map(e => e[0]),
- axisLabel: { color: '#ccc', fontSize: 10, interval: 0, rotate: 30 }
- },
- yAxis: {
- type: 'value', minInterval: 1,
- axisLabel: { color: '#ccc' },
- splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
- },
- series: [{ type: 'bar', data: entries.map(e => e[1]), itemStyle: { color: '#3399ff' }, barWidth: '40%' }]
- }, true)
- }
- function renderDeviceTypeChart() {
- if (!deviceTypeChartRef.value) return
- if (!deviceTypeChart) deviceTypeChart = echarts.init(deviceTypeChartRef.value)
- // 按台账实际存在的设备类型聚合,避免把不存在的旧类型当成固定三分类
- const countMap = new Map()
- allDevices.value.forEach(d => {
- const label = typeText(d.typeId, d.typeName)
- countMap.set(label, (countMap.get(label) || 0) + 1)
- })
- // 有设备但类型名为空时,至少给出一个占位分类,面板不留白
- if (!countMap.size && allDevices.value.length) {
- countMap.set('未分类', allDevices.value.length)
- }
- const labels = Array.from(countMap.keys())
- const values = Array.from(countMap.values())
- const palette = ['#3399ff', '#67c23a', '#e6a23c', '#f56c6c', '#9b59b6']
- deviceTypeChart.setOption({
- backgroundColor: 'transparent',
- tooltip: { trigger: 'axis' },
- grid: { left: '12%', right: '8%', bottom: '15%', top: '8%' },
- title: labels.length ? undefined : {
- text: '暂无窨井设备',
- subtext: '设备台账中未查到窨井类设备',
- left: 'center',
- top: 'middle',
- textStyle: { color: '#ccc', fontSize: 12, fontWeight: 'normal' },
- subtextStyle: { color: '#888', fontSize: 11 }
- },
- xAxis: {
- type: 'category', data: labels,
- axisLabel: { color: '#ccc', fontSize: 11, interval: 0 }
- },
- yAxis: {
- type: 'value', minInterval: 1,
- axisLabel: { color: '#ccc' },
- splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
- },
- series: labels.length ? [{
- type: 'bar',
- data: values.map((v, i) => ({ value: v, itemStyle: { color: palette[i % palette.length] } })),
- barWidth: '40%', label: { show: true, position: 'top', color: '#ccc' }
- }] : []
- }, true)
- }
- // ==================== 百度地图 ====================
- let mapInstance = null
- let mapFitted = false
- const markerMap = new Map()
- /** 兜底中心(沅陵县城),WGS84 口径,与设备台账一致 */
- const YUANLING_CENTER = { lng: 110.393, lat: 28.452 }
- function initMap() {
- const container = document.getElementById('homeMap')
- if (!container || typeof BMapGL === 'undefined') return
- try {
- mapInstance = new BMapGL.Map('homeMap')
- const [centerLng, centerLat] = convertCoord(YUANLING_CENTER.lng, YUANLING_CENTER.lat)
- mapInstance.centerAndZoom(new BMapGL.Point(centerLng, centerLat), 15)
- mapInstance.enableScrollWheelZoom(true)
- } catch (e) { console.error('地图初始化失败', e) }
- }
- /** 有设备时按设备范围取视野;点太近时限制放大层级,避免只剩一栋楼 */
- function fitMapToDevices(points) {
- if (!mapInstance || !points.length) return
- if (points.length === 1) {
- mapInstance.centerAndZoom(points[0], 16)
- return
- }
- const lngs = points.map(p => p.lng)
- const lats = points.map(p => p.lat)
- const span = Math.max(Math.max(...lngs) - Math.min(...lngs), Math.max(...lats) - Math.min(...lats))
- if (span < 0.002) {
- const mid = new BMapGL.Point((Math.max(...lngs) + Math.min(...lngs)) / 2, (Math.max(...lats) + Math.min(...lats)) / 2)
- mapInstance.centerAndZoom(mid, 16)
- } else {
- mapInstance.setViewport(points, { margins: [50, 50, 50, 50] })
- }
- }
- function getStatusColor(status) {
- if (status === 'alert') return '#F56C6C'
- if (status === 'offline') return '#909399'
- return '#67C23A'
- }
- function refreshMarkers() {
- if (!mapInstance) return
- markerMap.forEach(label => mapInstance.removeOverlay(label))
- markerMap.clear()
- const devicePoints = []
- allDevices.value.forEach(d => {
- if (d.lng == null || d.lat == null) return
- // 关键:台账是 WGS84,百度地图是 BD-09,不转换会偏约 1 公里
- const [bdLng, bdLat] = convertCoord(Number(d.lng), Number(d.lat))
- const point = new BMapGL.Point(bdLng, bdLat)
- devicePoints.push(point)
- const color = getStatusColor(d.status)
- const isAttention = d.status === 'alert' || d.status === 'offline'
- const attentionText = d.status === 'alert' ? '告警' : '离线'
- const html = isAttention
- ? `<div class="home-map-attention-marker home-map-${d.status}" title="${attentionText}设备:${d.name}">
- <div class="home-map-attention-badge">${attentionText} - ${d.name}</div>
- <div class="home-map-attention-pin">
- <i class="home-map-attention-ring"></i>
- <img src="${locationIcon}" alt="" />
- </div>
- </div>`
- : `<div style="text-align:center;cursor:pointer;">
- <div style="color:#fff;background:${color};border-radius:3px;padding:1px 6px;font-size:10px;white-space:nowrap;display:inline-block;margin-bottom:2px;box-shadow:0 1px 3px rgba(0,0,0,0.3);">${d.name}</div>
- <div><img src="${locationIcon}" alt="" style="width:22px;height:22px;display:block;margin:0 auto;"/></div>
- </div>`
- const label = new BMapGL.Label(html, {
- position: point,
- offset: new BMapGL.Size(isAttention ? -62 : -25, isAttention ? -68 : -40)
- })
- label.setStyle({ border: 'none', background: 'transparent', padding: '0', zIndex: isAttention ? '20' : '10' })
- mapInstance.addOverlay(label)
- markerMap.set(d.id, label)
- })
- // 只在首次拿到设备时校准视野,后续刷新不打断用户手动缩放
- if (!mapFitted && devicePoints.length) {
- fitMapToDevices(devicePoints)
- mapFitted = true
- }
- }
- // ==================== 生命周期 ====================
- const handleResize = () => {
- alarmChart && alarmChart.resize()
- areaChart && areaChart.resize()
- deviceTypeChart && deviceTypeChart.resize()
- mapInstance && mapInstance.resize && mapInstance.resize()
- }
- onMounted(async () => {
- await nextTick()
- initMap()
- window.addEventListener('resize', handleResize)
- loadAll()
- })
- onBeforeUnmount(() => {
- window.removeEventListener('resize', handleResize)
- alarmChart && alarmChart.dispose()
- areaChart && areaChart.dispose()
- deviceTypeChart && deviceTypeChart.dispose()
- if (mapInstance) {
- mapInstance.clearOverlays && mapInstance.clearOverlays()
- mapInstance = null
- }
- markerMap.clear()
- })
- </script>
- <!-- ==================== 全局样式(非 scoped):el-table 深色主题覆盖 ==================== -->
- <!-- 用 .monitor-container 限定范围,不影响其他页面 -->
- <style>
- .monitor-container .el-table {
- --el-table-bg-color: transparent;
- --el-table-tr-bg-color: transparent;
- --el-table-header-bg-color: transparent;
- --el-table-current-row-bg-color: rgba(0, 100, 200, 0.25);
- --el-table-row-hover-bg-color: rgba(0, 100, 200, 0.15);
- --el-table-text-color: #dce3ee;
- --el-table-header-text-color: #fff;
- --el-table-border-color: rgba(43, 75, 140, 0.35);
- --el-table-border: 1px solid rgba(43, 75, 140, 0.35);
- --el-fill-color-lighter: rgba(30, 40, 90, 0.25);
- --el-fill-color-light: rgba(30, 40, 90, 0.25);
- --el-fill-color: rgba(30, 40, 90, 0.25);
- --el-fill-color-blank: transparent;
- --el-text-color-regular: #dce3ee;
- --el-text-color-secondary: #9ba8c0;
- --el-border-color: rgba(43, 75, 140, 0.35);
- --el-border-color-lighter: rgba(43, 75, 140, 0.2);
- background: transparent !important;
- border: none !important;
- }
- /* 隐藏 EP 表格自带的底部/右侧边框伪元素 */
- .monitor-container .el-table::before,
- .monitor-container .el-table::after,
- .monitor-container .el-table .el-table__inner-wrapper::before,
- .monitor-container .el-table .el-table__inner-wrapper::after {
- display: none !important;
- }
- /* 内部容器全透明 */
- .monitor-container .el-table .el-table__inner-wrapper,
- .monitor-container .el-table .el-table__body-wrapper,
- .monitor-container .el-table .el-table__header-wrapper,
- .monitor-container .el-table .el-table__body,
- .monitor-container .el-table .el-table__header {
- background: transparent !important;
- }
- /* 表头单元格 — EP 2.x 使用 th.el-table__cell */
- .monitor-container .el-table th.el-table__cell {
- background: rgba(0, 100, 200, 0.35) !important;
- color: #fff !important;
- font-weight: 600;
- border-bottom: 1px solid #2b4b8c !important;
- }
- /* 内容单元格 */
- .monitor-container .el-table td.el-table__cell {
- background: transparent !important;
- border-bottom: 1px solid rgba(43, 75, 140, 0.25) !important;
- padding: 7px 0 !important;
- }
- /* 行 */
- .monitor-container .el-table .el-table__row {
- background: transparent !important;
- }
- .monitor-container .el-table .el-table__row:hover > td.el-table__cell {
- background: rgba(0, 100, 200, 0.15) !important;
- }
- /* 空数据 */
- .monitor-container .el-table .el-table__empty-block {
- background: transparent !important;
- }
- .monitor-container .el-table .el-table__empty-text {
- color: #8090a8;
- }
- /* 滚动条 */
- .monitor-container .el-table .el-scrollbar__bar {
- opacity: 0.4;
- }
- /* el-select 输入框深色适配 */
- .monitor-container .el-select .el-input__wrapper {
- background: rgba(30, 40, 90, 0.6);
- box-shadow: 0 0 0 1px rgba(43, 75, 140, 0.5) inset;
- }
- .monitor-container .el-select .el-input__inner {
- color: #dce3ee;
- }
- /* el-table 在 flex 容器中撑满高度(无 max-height 时) */
- .monitor-container .el-table {
- height: 100% !important;
- display: flex;
- flex-direction: column;
- }
- .monitor-container .el-table .el-table__inner-wrapper {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
- }
- .monitor-container .el-table .el-table__body-wrapper {
- flex: 1;
- min-height: 0;
- }
- /* 告警和离线设备地图标记:通过扩散圈和上下位移动画强化定位提示 */
- .home-map-attention-marker {
- position: relative;
- width: 124px;
- height: 70px;
- cursor: pointer;
- text-align: center;
- }
- .home-map-attention-badge {
- position: relative;
- z-index: 2;
- display: inline-block;
- max-width: 118px;
- overflow: hidden;
- padding: 2px 7px;
- border: 1px solid rgba(255, 255, 255, 0.72);
- border-radius: 4px;
- color: #fff;
- box-shadow: 0 2px 8px rgba(152, 23, 23, 0.55);
- font-size: 10px;
- line-height: 16px;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .home-map-attention-pin {
- position: absolute;
- left: 50%;
- top: 27px;
- width: 28px;
- height: 34px;
- transform: translateX(-50%);
- animation: homeMapAttentionBounce 1.1s ease-in-out infinite;
- }
- .home-map-attention-pin img {
- position: relative;
- z-index: 2;
- display: block;
- width: 28px;
- height: 28px;
- }
- .home-map-attention-ring {
- position: absolute;
- z-index: 1;
- left: 50%;
- top: 12px;
- width: 18px;
- height: 18px;
- border-radius: 50%;
- transform: translate(-50%, -50%);
- animation: homeMapAttentionPulse 1.5s ease-out infinite;
- }
- .home-map-alert .home-map-attention-badge { background: #e64545; }
- .home-map-alert .home-map-attention-ring { border: 2px solid rgba(255, 75, 75, 0.95); }
- .home-map-offline .home-map-attention-badge { background: #e67e22; }
- .home-map-offline .home-map-attention-ring { border: 2px solid rgba(255, 166, 77, 0.95); }
- @keyframes homeMapAttentionBounce {
- 0%, 100% { transform: translate(-50%, 0); }
- 50% { transform: translate(-50%, -8px); }
- }
- @keyframes homeMapAttentionPulse {
- 0% { opacity: 0.9; transform: translate(-50%, -50%) scale(0.7); }
- 70%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(2.2); }
- }
- </style>
- <!-- ==================== 页面布局样式(scoped) ==================== -->
- <style scoped>
- .monitor-container {
- display: flex;
- width: 100%;
- height: 100vh;
- min-height: 600px;
- background: #1a1a4a;
- color: #fff;
- gap: 16px;
- padding: 16px;
- box-sizing: border-box;
- overflow: hidden;
- }
- .left-col {
- flex: 0 0 25%;
- min-width: 280px;
- max-width: 420px;
- display: flex;
- flex-direction: column;
- gap: 16px;
- min-height: 0;
- }
- .right-col {
- flex: 0 0 25%;
- min-width: 280px;
- max-width: 420px;
- display: flex;
- flex-direction: column;
- gap: 16px;
- min-height: 0;
- }
- .center-col {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 16px;
- min-height: 0;
- }
- .panel {
- flex: 1;
- background: rgba(30, 40, 90, 0.8);
- border: 1px solid #2b4b8c;
- border-radius: 8px;
- padding: 14px;
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- min-height: 0;
- }
- /* 设备分类固定高度,不参与均分 */
- .chart-panel {
- flex: 0 0 190px;
- }
- .panel-title {
- margin: 0 0 10px 0;
- font-size: 14px;
- color: #66ccff;
- border-left: 3px solid #66ccff;
- padding-left: 8px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-shrink: 0;
- }
- .title-count {
- font-size: 12px;
- color: #aaa;
- font-weight: normal;
- }
- .panel-table {
- width: 100%;
- flex: 1;
- min-height: 0;
- overflow: hidden;
- }
- .chart {
- width: 100%;
- flex: 1;
- min-height: 100px;
- }
- .map-panel {
- flex: 1;
- display: flex;
- flex-direction: column;
- min-height: 0;
- }
- .map-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 8px;
- font-size: 14px;
- color: #66ccff;
- flex-shrink: 0;
- }
- .map-legend {
- display: flex;
- gap: 12px;
- font-size: 11px;
- }
- .legend-dot {
- display: inline-block;
- width: 8px;
- height: 8px;
- border-radius: 50%;
- margin-right: 3px;
- }
- .legend-dot.online { background: #67c23a; }
- .legend-dot.alert { background: #f56c6c; }
- .legend-dot.offline { background: #909399; }
- .bmap-container {
- width: 100%;
- flex: 1;
- min-height: 200px;
- border-radius: 4px;
- overflow: hidden;
- }
- .filter-bar {
- margin-bottom: 10px;
- flex-shrink: 0;
- }
- .priority-tag {
- padding: 1px 6px;
- border-radius: 3px;
- font-size: 11px;
- color: #fff;
- }
- .priority-tag.high { background: #ff3333; }
- .priority-tag.medium { background: #ffb333; color: #333; }
- .priority-tag.low { background: #33cc33; }
- .status-tag {
- padding: 1px 6px;
- border-radius: 3px;
- font-size: 11px;
- color: #fff;
- }
- .status-tag.processing { background: #3399ff; }
- .status-tag.done { background: #33cc33; }
- .status-dot {
- display: inline-block;
- width: 8px;
- height: 8px;
- border-radius: 50%;
- margin-right: 3px;
- }
- .status-dot.normal { background: #67c23a; }
- .status-dot.alert { background: #f56c6c; }
- .status-dot.offline { background: #909399; }
- .status-text {
- font-size: 11px;
- color: #dce3ee;
- }
- /* === 响应式 === */
- @media (max-width: 1600px) {
- .left-col, .right-col { flex: 0 0 24%; min-width: 260px; max-width: 380px; }
- .monitor-container { gap: 12px; padding: 12px; }
- .panel { padding: 12px; }
- .chart-panel { flex-basis: 170px; }
- }
- @media (max-width: 1366px) {
- .left-col, .right-col { flex: 0 0 27%; min-width: 240px; max-width: 340px; }
- .monitor-container { gap: 10px; padding: 10px; }
- .panel { padding: 10px; }
- .chart-panel { flex-basis: 150px; }
- }
- @media (max-width: 1024px) {
- .monitor-container {
- flex-direction: column;
- height: auto;
- min-height: 100vh;
- overflow: auto;
- }
- .left-col, .right-col {
- flex: 1 1 auto;
- min-width: 100%;
- max-width: 100%;
- }
- .center-col {
- flex: 1 1 auto;
- min-width: 100%;
- }
- }
- </style>
|