|
|
@@ -1,1376 +1,991 @@
|
|
|
-<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'
|
|
|
-import { convertPipeline, convertPoints } from '@/utils/coordTransform'
|
|
|
-import {
|
|
|
- getAlarmStats,
|
|
|
- getWaterFacilityFeatures,
|
|
|
- getWaterFacilityOverview,
|
|
|
- getDeviceGisAllPoints,
|
|
|
- getDeviceGisStatistics,
|
|
|
- listRealtimeAlarms,
|
|
|
- listDeviceLedger,
|
|
|
- listFlowMonitoring,
|
|
|
- listPressureMonitoring,
|
|
|
- listLeakageMonitoring
|
|
|
-} from '@/api/pipeNetwork/waterSupply'
|
|
|
-import { normalizeGeoJson, normalizeMonitoringRecord, normalizePage, unwrapResponse } from '@/utils/waterSupplyModel'
|
|
|
-
|
|
|
-const nowText = ref('')
|
|
|
-const weekText = ref('')
|
|
|
-const mapError = ref('')
|
|
|
-const loading = ref(false)
|
|
|
-
|
|
|
-const dashboardData = ref({})
|
|
|
-const pipelineLines = ref([])
|
|
|
-const pipePoints = ref([])
|
|
|
-
|
|
|
-const facilityTypes = [
|
|
|
- { label: '水源地', value: 'source', color: '#38a3ff' },
|
|
|
- { label: '水厂', value: 'plant', color: '#38d0a0' },
|
|
|
- { label: '泵站', value: 'pumpStation', color: '#ffc15e' },
|
|
|
- { label: '管网', value: 'pipe', color: '#7b9bb8' },
|
|
|
- { label: '用水户', value: 'user', color: '#ff7894' }
|
|
|
-]
|
|
|
-
|
|
|
-const typeCount = type => {
|
|
|
- const row = (dashboardData.value.facilityOverview || []).find(item => item.facilityType === type)
|
|
|
- return Number(row?.totalCount ?? row?.count ?? 0)
|
|
|
-}
|
|
|
-
|
|
|
-const baseCards = computed(() => {
|
|
|
- const fs = dashboardData.value.facilityOverview || []
|
|
|
- const es = dashboardData.value.equipmentStats || {}
|
|
|
- const alarm = dashboardData.value.alarmStats || {}
|
|
|
- const totalFacility = fs.reduce((sum, item) => sum + Number(item.totalCount || 0), 0)
|
|
|
- const totalDevice = Number(es.total ?? es.totalDeviceCount ?? 0)
|
|
|
- const onlineDevice = Number(es.online ?? es.onlineCount ?? 0)
|
|
|
- const realtimeAlarm = Number(alarm.realtimeAlarmCount ?? alarm.realtimeCount ?? 0)
|
|
|
- return [
|
|
|
- { label: '设施总数', value: totalFacility, unit: '处', icon: Odometer, color: '#60b8ff' },
|
|
|
- { label: '设备总数', value: totalDevice, unit: '台', icon: CircleCheckFilled, color: '#52e0a8' },
|
|
|
- { label: '在线设备', value: onlineDevice, unit: '台', icon: VideoCameraFilled, color: '#ff8e8e' },
|
|
|
- { label: '实时报警', value: realtimeAlarm, unit: '条', icon: WarnTriangleFilled, color: '#ffb54d' }
|
|
|
- ]
|
|
|
-})
|
|
|
-
|
|
|
-const operationSummary = computed(() => {
|
|
|
- return [
|
|
|
- { label: '供水管网', value: typeCount('pipe') + '条' },
|
|
|
- { label: '水厂', value: typeCount('plant') + '座' },
|
|
|
- { label: '泵站', value: typeCount('pumpStation') + '座' }
|
|
|
- ]
|
|
|
-})
|
|
|
-
|
|
|
-const repairRecords = computed(() => {
|
|
|
- const list = dashboardData.value.facilityOverview || []
|
|
|
- if (list.length) {
|
|
|
- return list.map(item => ({ month: item.facilityTypeName || item.facilityType || '设施', value: Number(item.totalCount || 0) }))
|
|
|
- }
|
|
|
- return [
|
|
|
- { month: '水源地', value: 0 }, { month: '水厂', value: 0 }, { month: '泵站', value: 0 },
|
|
|
- { month: '管网', value: 0 }, { month: '用水户', value: 0 }
|
|
|
- ]
|
|
|
-})
|
|
|
-
|
|
|
-const pointRows = computed(() => {
|
|
|
- const list = dashboardData.value.pointRows || []
|
|
|
- return list.length > 0 ? list : [{ id: 1, name: '暂无数据', time: '--', value: '--', status: '离线' }]
|
|
|
-})
|
|
|
-
|
|
|
-const equipmentRows = computed(() => {
|
|
|
- const list = dashboardData.value.equipmentRows || []
|
|
|
- return list.length > 0 ? list : [{ id: 1, name: '暂无数据', code: '--', location: '--', status: '--' }]
|
|
|
-})
|
|
|
-
|
|
|
-const warningRows = computed(() => {
|
|
|
- const list = dashboardData.value.warningRows || []
|
|
|
- return list.length > 0 ? list : [{ level: '中', title: '暂无告警', code: '--', time: '--:--:--', desc: '当前无告警信息', tag: '正常' }]
|
|
|
-})
|
|
|
-
|
|
|
-const hazardSummary = computed(() => {
|
|
|
- const alarm = dashboardData.value.alarmStats || {}
|
|
|
- return [
|
|
|
- { label: '实时报警', value: alarm.realtimeAlarmCount ?? alarm.realtimeCount ?? 0, color: '#ff6d79' },
|
|
|
- { label: '今日报警', value: alarm.todayAlarmCount ?? alarm.todayCount ?? 0, color: '#ffb54d' },
|
|
|
- { label: '异常率', value: alarm.deviceAbnormalRate ?? alarm.abnormalRate ?? 0, color: '#f3e64d' }
|
|
|
- ]
|
|
|
-})
|
|
|
-
|
|
|
-const mapKpis = computed(() => {
|
|
|
- const es = dashboardData.value.equipmentStats || {}
|
|
|
- const alarm = dashboardData.value.alarmStats || {}
|
|
|
- const totalDevice = Number(es.total ?? es.totalDeviceCount ?? 0)
|
|
|
- const onlineDevice = Number(es.online ?? es.onlineCount ?? 0)
|
|
|
- return [
|
|
|
- { label: '设备在线率', value: totalDevice > 0 ? (onlineDevice / totalDevice * 100).toFixed(1) + '%' : '--' },
|
|
|
- { label: '供水设施', value: dashboardData.value.facilityOverview?.reduce((sum, item) => sum + Number(item.totalCount || 0), 0) || 0 },
|
|
|
- { label: '今日报警', value: (alarm.todayAlarmCount ?? alarm.todayCount ?? 0) + '条' }
|
|
|
- ]
|
|
|
-})
|
|
|
-
|
|
|
-const mapWrapRef = ref(null)
|
|
|
-const repairChartRef = ref(null)
|
|
|
-const hazardChartRef = ref(null)
|
|
|
-let mapInstance = null
|
|
|
-let repairChart = null
|
|
|
-let hazardChart = null
|
|
|
-let clockTimer = null
|
|
|
-
|
|
|
-function formatWeek(day) {
|
|
|
- return ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][day]
|
|
|
-}
|
|
|
-
|
|
|
-function pad(value) {
|
|
|
- return String(value).padStart(2, '0')
|
|
|
-}
|
|
|
-
|
|
|
-function updateClock() {
|
|
|
- const now = new Date()
|
|
|
- nowText.value = `${now.getFullYear()}/${pad(now.getMonth() + 1)}/${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
|
|
|
- weekText.value = formatWeek(now.getDay())
|
|
|
-}
|
|
|
-
|
|
|
-function initRepairChart() {
|
|
|
- if (!repairChartRef.value) return
|
|
|
- repairChart?.dispose()
|
|
|
- repairChart = echarts.init(repairChartRef.value)
|
|
|
- const data = repairRecords.value
|
|
|
- repairChart.setOption({
|
|
|
- grid: { left: 30, right: 12, top: 20, bottom: 24, containLabel: true },
|
|
|
- xAxis: {
|
|
|
- type: 'category',
|
|
|
- data: data.map(item => item.month),
|
|
|
- axisLine: { lineStyle: { color: 'rgba(91, 167, 219, 0.8)' } },
|
|
|
- axisLabel: { color: '#d2efff' }
|
|
|
- },
|
|
|
- yAxis: {
|
|
|
- type: 'value',
|
|
|
- splitLine: { lineStyle: { color: 'rgba(91, 167, 219, 0.18)' } },
|
|
|
- axisLine: { show: false },
|
|
|
- axisLabel: { color: '#d2efff' }
|
|
|
- },
|
|
|
- tooltip: { trigger: 'axis' },
|
|
|
- series: [{
|
|
|
- type: 'bar',
|
|
|
- barWidth: 24,
|
|
|
- data: data.map(item => item.value),
|
|
|
- itemStyle: {
|
|
|
- borderRadius: [6, 6, 0, 0],
|
|
|
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
- { offset: 0, color: '#38d8ff' },
|
|
|
- { offset: 1, color: '#166dff' }
|
|
|
- ])
|
|
|
- }
|
|
|
- }]
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function initHazardChart() {
|
|
|
- if (!hazardChartRef.value) return
|
|
|
- hazardChart?.dispose()
|
|
|
- hazardChart = echarts.init(hazardChartRef.value)
|
|
|
- const data = hazardSummary.value
|
|
|
- hazardChart.setOption({
|
|
|
- grid: { left: 18, right: 18, top: 10, bottom: 10, containLabel: true },
|
|
|
- xAxis: {
|
|
|
- type: 'value',
|
|
|
- splitLine: { show: false },
|
|
|
- axisLine: { show: false },
|
|
|
- axisTick: { show: false },
|
|
|
- axisLabel: { show: false }
|
|
|
- },
|
|
|
- yAxis: {
|
|
|
- type: 'category',
|
|
|
- data: data.map(item => item.label),
|
|
|
- axisLine: { show: false },
|
|
|
- axisTick: { show: false },
|
|
|
- axisLabel: { color: '#d2efff', fontSize: 13 }
|
|
|
- },
|
|
|
- tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
|
|
- series: [{
|
|
|
- type: 'bar',
|
|
|
- data: data.map(item => ({ value: item.value, itemStyle: { color: item.color, borderRadius: 10 } })),
|
|
|
- barWidth: 14,
|
|
|
- label: { show: true, position: 'right', color: '#fff', fontWeight: 'bold' },
|
|
|
- showBackground: true,
|
|
|
- backgroundStyle: { color: 'rgba(255,255,255,0.08)', borderRadius: 10 }
|
|
|
- }]
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function waitForBMapGL() {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- if (window.BMapGL) {
|
|
|
- resolve(window.BMapGL)
|
|
|
- return
|
|
|
- }
|
|
|
- let attempts = 0
|
|
|
- const timer = setInterval(() => {
|
|
|
- attempts += 1
|
|
|
- if (window.BMapGL) {
|
|
|
- clearInterval(timer)
|
|
|
- resolve(window.BMapGL)
|
|
|
- return
|
|
|
- }
|
|
|
- if (attempts >= 100) {
|
|
|
- clearInterval(timer)
|
|
|
- reject(new Error('百度地图加载超时'))
|
|
|
- }
|
|
|
- }, 200)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function addPipeLine(BMapGL, pipe) {
|
|
|
- const bmapPoints = pipe.points.map(([lng, lat]) => new BMapGL.Point(lng, lat))
|
|
|
- const polyline = new BMapGL.Polyline(bmapPoints, {
|
|
|
- strokeColor: pipe.color,
|
|
|
- strokeWeight: 4,
|
|
|
- strokeOpacity: 0.95
|
|
|
- })
|
|
|
- mapInstance.addOverlay(polyline)
|
|
|
-
|
|
|
- bmapPoints.forEach((point) => {
|
|
|
- const ring = new BMapGL.Circle(point, 14, {
|
|
|
- strokeColor: pipe.color,
|
|
|
- strokeWeight: 2,
|
|
|
- strokeOpacity: 0.7,
|
|
|
- fillColor: 'rgba(56,182,233,0.08)',
|
|
|
- fillOpacity: 0.18
|
|
|
- })
|
|
|
- mapInstance.addOverlay(ring)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function addPipePoint(BMapGL, point) {
|
|
|
- const markerPoint = new BMapGL.Point(point.lng, point.lat)
|
|
|
- const marker = new BMapGL.Marker(markerPoint)
|
|
|
- mapInstance.addOverlay(marker)
|
|
|
- const label = new BMapGL.Label(`${point.name} ${point.value}`, { offset: new BMapGL.Size(-42, -40) })
|
|
|
- label.setPosition(markerPoint)
|
|
|
- label.setStyle({
|
|
|
- color: '#d8faff',
|
|
|
- background: 'rgba(18, 68, 112, 0.75)',
|
|
|
- border: '1px solid rgba(56, 182, 233, 0.55)',
|
|
|
- borderRadius: '8px',
|
|
|
- padding: '5px 8px',
|
|
|
- fontSize: '12px'
|
|
|
- })
|
|
|
- mapInstance.addOverlay(label)
|
|
|
-}
|
|
|
-
|
|
|
-async function initMap() {
|
|
|
- if (!mapWrapRef.value) return
|
|
|
- try {
|
|
|
- const BMapGL = await waitForBMapGL()
|
|
|
- mapInstance = new BMapGL.Map(mapWrapRef.value, { enableMapClick: true })
|
|
|
- const center = new BMapGL.Point(110.3965, 28.4638)
|
|
|
- mapInstance.centerAndZoom(center, 15)
|
|
|
- mapInstance.enableScrollWheelZoom(true)
|
|
|
- mapInstance.setMapStyleV2({ styleId: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' })
|
|
|
-
|
|
|
- const bdPipelineLines = pipelineLines.value.map(pipe => convertPipeline(pipe))
|
|
|
- const bdPipePoints = convertPoints(pipePoints.value)
|
|
|
- bdPipelineLines.forEach(pipe => addPipeLine(BMapGL, pipe))
|
|
|
- bdPipePoints.forEach(point => addPipePoint(BMapGL, point))
|
|
|
- } catch (error) {
|
|
|
- mapError.value = '百度地图加载失败,请检查地图脚本或网络连接。'
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function handleResize() {
|
|
|
- repairChart?.resize()
|
|
|
- hazardChart?.resize()
|
|
|
-}
|
|
|
-
|
|
|
-function parseFeatures(features) {
|
|
|
- const lines = []
|
|
|
- const points = []
|
|
|
- ;(features || []).forEach(feature => {
|
|
|
- const coordinates = feature.geometry?.coordinates || []
|
|
|
- const properties = feature.properties || {}
|
|
|
- if (feature.geometry?.type === 'LineString' && coordinates.length > 1) {
|
|
|
- lines.push({
|
|
|
- id: feature.id || `line-${lines.length}`,
|
|
|
- name: properties.facilityTypeName || properties.name || '供水管网',
|
|
|
- color: '#37b6e9',
|
|
|
- points: coordinates
|
|
|
- })
|
|
|
- }
|
|
|
- if (feature.geometry?.type === 'Point' && coordinates.length >= 2) {
|
|
|
- points.push({
|
|
|
- id: feature.id || properties.id,
|
|
|
- name: properties.facilityName || properties.name || properties.facilityTypeName || '供水设施',
|
|
|
- value: properties.facilityTypeName || properties.facilityType || '',
|
|
|
- lng: coordinates[0],
|
|
|
- lat: coordinates[1],
|
|
|
- type: properties.facilityType,
|
|
|
- properties
|
|
|
- })
|
|
|
- }
|
|
|
- })
|
|
|
- pipelineLines.value = lines
|
|
|
- pipePoints.value = points
|
|
|
-}
|
|
|
-
|
|
|
-function buildPointRows(flowPage, pressurePage, leakagePage) {
|
|
|
- const flow = normalizePage(flowPage).records.map(item => normalizeMonitoringRecord(item, 'flow'))
|
|
|
- const pressure = normalizePage(pressurePage).records.map(item => normalizeMonitoringRecord(item, 'pressure'))
|
|
|
- const leakage = normalizePage(leakagePage).records.map(item => normalizeMonitoringRecord(item, 'power'))
|
|
|
- return [...flow, ...pressure, ...leakage].slice(0, 8).map(item => ({
|
|
|
- id: item.id,
|
|
|
- name: item.equipmentName,
|
|
|
- time: item.updateTime,
|
|
|
- value: item.value == null ? '--' : item.value,
|
|
|
- status: item.status === 1 || item.status === '1' ? '预警' : '在线'
|
|
|
- }))
|
|
|
-}
|
|
|
-
|
|
|
-async function loadDashboardData() {
|
|
|
- loading.value = true
|
|
|
- try {
|
|
|
- const [overviewResponse, featuresResponse, devicePointsResponse, deviceStatsResponse, alarmResponse, alarmSummaryResponse, devicePageResponse, flowResponse, pressureResponse, leakageResponse] = await Promise.all([
|
|
|
- getWaterFacilityOverview(),
|
|
|
- getWaterFacilityFeatures(),
|
|
|
- getDeviceGisAllPoints(),
|
|
|
- getDeviceGisStatistics(),
|
|
|
- listRealtimeAlarms(),
|
|
|
- getAlarmStats(),
|
|
|
- listDeviceLedger({ pageNum: 1, pageSize: 8 }),
|
|
|
- listFlowMonitoring({ pageNum: 1, pageSize: 5 }),
|
|
|
- listPressureMonitoring({ pageNum: 1, pageSize: 5 }),
|
|
|
- listLeakageMonitoring({ pageNum: 1, pageSize: 5 })
|
|
|
- ])
|
|
|
- const overviewData = unwrapResponse(overviewResponse)
|
|
|
- const facilityOverview = Array.isArray(overviewData) ? overviewData : (overviewData?.facilityOverview || [])
|
|
|
- const features = normalizeGeoJson(featuresResponse).features || []
|
|
|
- const deviceData = unwrapResponse(devicePointsResponse)
|
|
|
- const deviceRows = Array.isArray(deviceData) ? deviceData : (deviceData?.features || deviceData?.records || [])
|
|
|
- const devicePage = normalizePage(devicePageResponse)
|
|
|
- const rawStats = unwrapResponse(deviceStatsResponse) || {}
|
|
|
- const typeStatistics = rawStats.typeStatistics || []
|
|
|
- const flatten = list => (Array.isArray(list) ? list : []).reduce((sum, item) => sum + Number(item.deviceCount || item.count || 0) + flatten(item.children), 0)
|
|
|
- const equipmentStats = {
|
|
|
- ...rawStats,
|
|
|
- total: Number(rawStats.total ?? rawStats.totalDeviceCount ?? 0) || flatten(typeStatistics) || deviceRows.length,
|
|
|
- online: Number(rawStats.online ?? rawStats.onlineCount ?? deviceRows.filter(item => Number(item.onlineStatus) === 1).length),
|
|
|
- abnormal: Number(rawStats.abnormal ?? rawStats.abnormalCount ?? deviceRows.filter(item => Number(item.alarmStatus) === 1).length)
|
|
|
- }
|
|
|
- const alarmList = Array.isArray(unwrapResponse(alarmResponse)) ? unwrapResponse(alarmResponse) : []
|
|
|
- const alarmStats = unwrapResponse(alarmSummaryResponse) || {}
|
|
|
- const equipmentRows = devicePage.records.slice(0, 8).map(item => ({
|
|
|
- id: item.equipmentId || item.id,
|
|
|
- name: item.equipmentName || item.name || '-',
|
|
|
- code: item.equipmentCode || item.code || '-',
|
|
|
- location: item.equipmentLocation || item.location || '-',
|
|
|
- status: Number(item.status || item.onlineStatus) === 1 ? '异常' : '正常'
|
|
|
- }))
|
|
|
- const warningRows = alarmList.slice(0, 6).map(item => ({
|
|
|
- level: Number(item.alarmLevel) <= 2 ? '高' : '中',
|
|
|
- title: item.deviceName || item.deviceCode || '监测设备',
|
|
|
- code: item.warningType || item.alarmCode || '-',
|
|
|
- time: item.alarmTime || '--',
|
|
|
- desc: item.equipmentLocation || '报警详情待查看',
|
|
|
- tag: Number(item.alarmLevel) <= 2 ? '紧急' : '一般'
|
|
|
- }))
|
|
|
- dashboardData.value = {
|
|
|
- facilityOverview,
|
|
|
- equipmentStats,
|
|
|
- alarmStats,
|
|
|
- warningRows,
|
|
|
- equipmentRows,
|
|
|
- pointRows: buildPointRows(flowResponse, pressureResponse, leakageResponse)
|
|
|
- }
|
|
|
- parseFeatures(features)
|
|
|
- } catch (error) {
|
|
|
- console.warn('供水首页数据加载失败', error)
|
|
|
- dashboardData.value = { facilityOverview: [], equipmentStats: {}, alarmStats: {}, warningRows: [], equipmentRows: [], pointRows: [] }
|
|
|
- pipelineLines.value = []
|
|
|
- pipePoints.value = []
|
|
|
- } finally {
|
|
|
- loading.value = false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-onMounted(async () => {
|
|
|
- updateClock()
|
|
|
- clockTimer = window.setInterval(updateClock, 1000)
|
|
|
- await loadDashboardData()
|
|
|
- await nextTick()
|
|
|
- initRepairChart()
|
|
|
- initHazardChart()
|
|
|
- initMap()
|
|
|
- window.addEventListener('resize', handleResize)
|
|
|
-})
|
|
|
-
|
|
|
-onBeforeUnmount(() => {
|
|
|
- if (clockTimer) window.clearInterval(clockTimer)
|
|
|
- window.removeEventListener('resize', handleResize)
|
|
|
- repairChart?.dispose()
|
|
|
- hazardChart?.dispose()
|
|
|
- mapInstance = null
|
|
|
-})
|
|
|
-</script>
|
|
|
-
|
|
|
-
|
|
|
<template>
|
|
|
- <div class="gas-home-screen">
|
|
|
- <div class="screen-header">
|
|
|
- <div class="header-time">{{ nowText }} {{ weekText }}</div>
|
|
|
- <div class="header-title-wrap">
|
|
|
- <div class="header-title">供水管网安全运行监测系统</div>
|
|
|
- <div class="header-subtitle">Water Supply Operation Dashboard</div>
|
|
|
+ <div class="water-page home-page">
|
|
|
+ <div class="page-heading">
|
|
|
+ <div class="page-title">
|
|
|
+ <span class="page-title-icon"><el-icon><HomeFilled /></el-icon></span>
|
|
|
+ <div>
|
|
|
+ <h2>供水首页工作台</h2>
|
|
|
+ <span class="muted">面向值班态的一站式供水运行工作台,仅统计供水设施与供水监测设备</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div class="header-tags">
|
|
|
- <span class="header-tag">实时监测</span>
|
|
|
- <span class="header-tag">供水设施</span>
|
|
|
- <span class="header-tag">报警预警</span>
|
|
|
+ <div class="heading-actions">
|
|
|
+ <el-button :icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <div class="screen-body">
|
|
|
- <div class="side-panel left-panel">
|
|
|
- <section class="panel-card base-card">
|
|
|
- <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" :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>
|
|
|
- </div>
|
|
|
+ <el-alert v-if="loadError" class="load-error" type="error" show-icon :closable="false"
|
|
|
+ title="部分数据加载失败" :description="loadError">
|
|
|
+ <el-button size="small" type="primary" :loading="loading" @click="loadAll">重试</el-button>
|
|
|
+ </el-alert>
|
|
|
+
|
|
|
+ <!-- A 指挥台 -->
|
|
|
+ <section class="home-section">
|
|
|
+ <div class="section-heading"><el-icon><Monitor /></el-icon><span>A · 指挥台</span></div>
|
|
|
+ <div class="metric-grid">
|
|
|
+ <div v-for="card in homeKpis" :key="card.key" class="metric">
|
|
|
+ <div class="metric-icon"><el-icon><component :is="kpiIcon(card.key)"/></el-icon></div>
|
|
|
+ <div class="metric-body">
|
|
|
+ <span>{{ card.label }}</span>
|
|
|
+ <div class="metric-value">
|
|
|
+ <strong :class="card.tone">{{ card.value }}</strong><small>{{ card.unit }}</small>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section class="panel-card">
|
|
|
- <div class="panel-title">设施统计</div>
|
|
|
- <div ref="repairChartRef" class="chart-box repair-chart"></div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section class="panel-card table-card">
|
|
|
- <div class="panel-title">监测点数据</div>
|
|
|
- <div class="table-wrap">
|
|
|
- <table class="data-table">
|
|
|
- <thead>
|
|
|
- <tr>
|
|
|
- <th>序号</th>
|
|
|
- <th>监测点</th>
|
|
|
- <th>时间</th>
|
|
|
- <th>数值</th>
|
|
|
- <th>状态</th>
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
- <tbody>
|
|
|
- <tr v-for="row in pointRows" :key="row.id">
|
|
|
- <td>{{ row.id }}</td>
|
|
|
- <td>{{ row.name }}</td>
|
|
|
- <td>{{ row.time }}</td>
|
|
|
- <td>{{ row.value }}</td>
|
|
|
- <td :class="['status-cell', row.status === '在线' ? 'ok' : row.status === '预警' ? 'warn' : 'off']">{{ row.status }}</td>
|
|
|
- </tr>
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
- </div>
|
|
|
- </section>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
-
|
|
|
- <div class="center-panel">
|
|
|
- <div class="map-stage">
|
|
|
- <div class="map-stage-top">
|
|
|
- <div class="summary-strip">
|
|
|
- <div v-for="item in operationSummary" :key="item.label" class="summary-item">
|
|
|
- <span class="summary-label">{{ item.label }}</span>
|
|
|
- <strong class="summary-value">{{ item.value }}</strong>
|
|
|
- </div>
|
|
|
+ <div class="workbench-grid">
|
|
|
+ <el-card shadow="never" class="water-panel home-panel">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-title">
|
|
|
+ <el-icon><Bell /></el-icon><span>实时报警</span>
|
|
|
+ <el-button link type="primary" class="panel-link" @click="goPage('/monitorAlarm/index')">前往报警管理</el-button>
|
|
|
</div>
|
|
|
- <div class="map-kpis">
|
|
|
- <div v-for="item in mapKpis" :key="item.label" class="map-kpi">
|
|
|
- <span>{{ item.label }}</span>
|
|
|
- <strong>{{ item.value }}</strong>
|
|
|
+ </template>
|
|
|
+ <div class="compact-list">
|
|
|
+ <button v-for="item in alarmRows" :key="item.id || item.deviceCode" type="button"
|
|
|
+ class="compact-item" @click="goPage('/monitorAlarm/audit', { alarmId: item.id || undefined, deviceCode: item.deviceCode })">
|
|
|
+ <span class="status-dot dot-alarm" />
|
|
|
+ <span class="compact-main">
|
|
|
+ <b>{{ item.deviceName }}</b>
|
|
|
+ <small>{{ item.deviceCode }}</small>
|
|
|
+ </span>
|
|
|
+ <span class="compact-meta">
|
|
|
+ <el-tag size="small" :type="levelType(item.alarmLevel)">{{ item.alarmLevelText }}</el-tag>
|
|
|
+ <small>{{ item.alarmTime }}</small>
|
|
|
+ </span>
|
|
|
+ </button>
|
|
|
+ <el-empty v-if="!alarmRows.length" description="当前无实时报警" :image-size="60" />
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card shadow="never" class="water-panel home-panel map-panel">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-title">
|
|
|
+ <el-icon><MapLocation /></el-icon><span>全域态势</span>
|
|
|
+ <div class="map-legend">
|
|
|
+ <i class="legend-dot dot-alarm" />报警
|
|
|
+ <i class="legend-dot dot-offline" />离线
|
|
|
+ <i class="legend-dot dot-normal" />正常
|
|
|
</div>
|
|
|
+ <el-button link type="primary" class="panel-link" @click="goPage('/device/gis')">监测设备一张图</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="map-stage">
|
|
|
+ <div v-for="point in overviewMapPoints" :key="point.id" class="map-point"
|
|
|
+ :class="`state-${point.state}`" :style="{ left: point.mapX + '%', top: point.mapY + '%' }"
|
|
|
+ :title="`${point.name} · ${point.meta}`">
|
|
|
+ <span />
|
|
|
+ <b>{{ point.name }}</b>
|
|
|
</div>
|
|
|
+ <el-empty v-if="!overviewMapPoints.length" description="暂无可展示的设备点位" :image-size="70" />
|
|
|
+ <div class="map-note">点位归一化示意 · 精确位置请进入各一张图</div>
|
|
|
</div>
|
|
|
+ </el-card>
|
|
|
|
|
|
- <div ref="mapWrapRef" class="map-view"></div>
|
|
|
- <div v-if="mapError" class="map-error">{{ mapError }}</div>
|
|
|
-
|
|
|
- <div class="map-panel info-panel">
|
|
|
- <div class="mini-title">供水设施信息</div>
|
|
|
- <div class="mini-row"><span>供水管网</span><strong>{{ typeCount('pipe') }}条</strong></div>
|
|
|
- <div class="mini-row"><span>水厂</span><strong>{{ typeCount('plant') }}座</strong></div>
|
|
|
- <div class="mini-row"><span>泵站</span><strong>{{ typeCount('pumpStation') }}座</strong></div>
|
|
|
+ <el-card shadow="never" class="water-panel home-panel">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-title">
|
|
|
+ <el-icon><Tickets /></el-icon><span>工单进展</span>
|
|
|
+ <el-button link type="primary" class="panel-link" @click="goPage('/monitorAlarm/workOrder')">统一工单</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="compact-list">
|
|
|
+ <button v-for="item in workOrderRows" :key="item.orderId || item.orderNo" type="button"
|
|
|
+ class="compact-item" @click="goPage('/monitorAlarm/workOrder', { keyword: item.orderNo })">
|
|
|
+ <span class="status-dot" :class="dotByOrderType(item.orderStatusType)" />
|
|
|
+ <span class="compact-main">
|
|
|
+ <b>{{ item.orderNo }}</b>
|
|
|
+ <small>{{ item.deviceName || item.deviceCode }}</small>
|
|
|
+ </span>
|
|
|
+ <span class="compact-meta">
|
|
|
+ <el-tag size="small" :type="item.orderStatusType">{{ item.orderStatusText }}</el-tag>
|
|
|
+ </span>
|
|
|
+ </button>
|
|
|
+ <el-empty v-if="!workOrderRows.length" description="当前暂无供水工单" :image-size="60" />
|
|
|
</div>
|
|
|
-
|
|
|
- <div class="map-panel point-panel">
|
|
|
- <div class="mini-title">当前监测信息</div>
|
|
|
- <div class="mini-row"><span>监测点总数</span><strong>{{ pointRows.length }}个</strong></div>
|
|
|
- <div class="mini-row"><span>在线设备</span><strong>{{ dashboardData.equipmentStats?.online || 0 }}台</strong></div>
|
|
|
- <div class="mini-row"><span>实时报警</span><strong>{{ dashboardData.alarmStats?.realtimeAlarmCount ?? dashboardData.alarmStats?.realtimeCount ?? 0 }}条</strong></div>
|
|
|
+ <div class="order-brief">
|
|
|
+ <span>待处理 <b>{{ workOrderStat.pendingCount }}</b></span>
|
|
|
+ <span>处理中 <b>{{ workOrderStat.processingCount }}</b></span>
|
|
|
+ <span>待验收 <b>{{ workOrderStat.verifyingCount }}</b></span>
|
|
|
+ <span>已完成 <b>{{ workOrderStat.finishedCount }}</b></span>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ </el-card>
|
|
|
</div>
|
|
|
-
|
|
|
- <div class="side-panel right-panel">
|
|
|
- <section class="panel-card">
|
|
|
- <div class="panel-title">设备运行</div>
|
|
|
- <div class="table-wrap equipment-wrap">
|
|
|
- <table class="data-table compact-table">
|
|
|
- <thead>
|
|
|
- <tr>
|
|
|
- <th>序号</th>
|
|
|
- <th>设备名称</th>
|
|
|
- <th>设备编号</th>
|
|
|
- <th>位置</th>
|
|
|
- <th>状态</th>
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
- <tbody>
|
|
|
- <tr v-for="row in equipmentRows" :key="row.id">
|
|
|
- <td>{{ row.id }}</td>
|
|
|
- <td>{{ row.name }}</td>
|
|
|
- <td>{{ row.code }}</td>
|
|
|
- <td>{{ row.location }}</td>
|
|
|
- <td>{{ row.status }}</td>
|
|
|
- </tr>
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- B 异常待办 -->
|
|
|
+ <section class="home-section">
|
|
|
+ <div class="section-heading"><el-icon><WarningFilled /></el-icon><span>B · 异常待办</span></div>
|
|
|
+ <div class="workbench-grid">
|
|
|
+ <el-card shadow="never" class="water-panel home-panel">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-title">
|
|
|
+ <el-icon><Warning /></el-icon><span>异常待办</span>
|
|
|
+ <el-button link type="primary" class="panel-link" @click="goPage('/device/abnormal')">查看全部</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="compact-list">
|
|
|
+ <button v-for="item in abnormalTodoRows" :key="item.equipmentId" type="button"
|
|
|
+ class="compact-item" :class="{ active: selectedDevice && selectedDevice.equipmentId === item.equipmentId }"
|
|
|
+ @click="selectAbnormal(item)">
|
|
|
+ <span class="status-dot" :class="abnormalDot(item)" />
|
|
|
+ <span class="compact-main">
|
|
|
+ <b>{{ item.equipmentName || item.equipmentCode }}</b>
|
|
|
+ <small>{{ item.equipmentCode }}</small>
|
|
|
+ </span>
|
|
|
+ <span class="compact-meta">
|
|
|
+ <el-tag size="small" :type="abnormalType(item)">{{ item.abnormalReasonText || '设备异常' }}</el-tag>
|
|
|
+ </span>
|
|
|
+ </button>
|
|
|
+ <el-empty v-if="!abnormalTodoRows.length" description="当前无异常设备待办" :image-size="60" />
|
|
|
</div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section class="panel-card warning-card">
|
|
|
- <div class="panel-title">告警动态</div>
|
|
|
- <div class="warning-scroll">
|
|
|
- <div class="warning-list">
|
|
|
- <div v-for="(item, index) in warningRows" :key="index" class="warning-item" :class="item.level === '高' ? 'danger' : 'notice'">
|
|
|
- <div class="warning-side">
|
|
|
- <div class="warning-badge">{{ item.level }}</div>
|
|
|
- <div class="warning-tag">{{ item.tag }}</div>
|
|
|
- </div>
|
|
|
- <div class="warning-main">
|
|
|
- <div class="warning-title-row">
|
|
|
- <span class="warning-title">{{ item.title }}</span>
|
|
|
- <span class="warning-time">{{ item.time }}</span>
|
|
|
- </div>
|
|
|
- <div class="warning-code">{{ item.code }}</div>
|
|
|
- <div class="warning-desc">{{ item.desc }}</div>
|
|
|
- </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card shadow="never" class="water-panel home-panel detail-panel">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-title"><el-icon><Cpu /></el-icon><span>设备详情</span></div>
|
|
|
+ </template>
|
|
|
+ <div v-if="selectedDevice" class="device-summary">
|
|
|
+ <div class="summary-top">
|
|
|
+ <div class="summary-name">{{ selectedDevice.name }}</div>
|
|
|
+ <div class="summary-tags">
|
|
|
+ <el-tag size="small" :type="Number(selectedDevice.alarmStatus) === 1 ? 'danger' : 'success'">{{ selectedDevice.alarmStatusText }}</el-tag>
|
|
|
+ <el-tag size="small" :type="Number(selectedDevice.onlineStatus) === 1 ? 'success' : 'info'">{{ selectedDevice.onlineStatusText }}</el-tag>
|
|
|
+ <el-tag v-if="selectedDevice.relatedOrderNo && selectedDevice.relatedOrderNo !== '-'" size="small"
|
|
|
+ :type="selectedDevice.workOrderStatusCode != null ? orderType(selectedDevice.workOrderStatusCode) : 'info'">
|
|
|
+ {{ selectedDevice.workOrderStatusText }}
|
|
|
+ </el-tag>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <el-descriptions :column="1" border size="small">
|
|
|
+ <el-descriptions-item label="设备编码">{{ selectedDevice.code }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备类型">{{ selectedDevice.typeName }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="所属区域">{{ selectedDevice.district || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="安装位置">{{ selectedDevice.location || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="报警等级">{{ selectedDevice.alarmLevelText }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="异常原因">{{ selectedDevice.abnormalReasonText || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="关联工单">
|
|
|
+ <el-button v-if="selectedDevice.relatedOrderNo && selectedDevice.relatedOrderNo !== '-'" link type="primary"
|
|
|
+ @click="goPage('/monitorAlarm/workOrder', { keyword: selectedDevice.relatedOrderNo })">
|
|
|
+ {{ selectedDevice.relatedOrderNo }}
|
|
|
+ </el-button>
|
|
|
+ <span v-else>-</span>
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+ <el-empty v-else description="从左侧异常待办中选择设备" :image-size="70" />
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card shadow="never" class="water-panel home-panel">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-title"><el-icon><Grid /></el-icon><span>快捷入口</span></div>
|
|
|
+ </template>
|
|
|
+ <div class="quick-grid">
|
|
|
+ <button v-for="entry in quickEntries" :key="entry.key" type="button" class="quick-entry"
|
|
|
+ @click="goPage(entry.suffix)">
|
|
|
+ <el-icon><component :is="entryIcon(entry.key)"/></el-icon>
|
|
|
+ <span>{{ entry.label }}</span>
|
|
|
+ </button>
|
|
|
</div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section class="panel-card hazard-card">
|
|
|
- <div class="panel-title">报警统计</div>
|
|
|
- <div class="hazard-summary three-col">
|
|
|
- <div v-for="item in hazardSummary" :key="item.label" class="hazard-summary-item">
|
|
|
- <span class="hazard-dot" :style="{ background: item.color }"></span>
|
|
|
- <span>{{ item.label }}</span>
|
|
|
- <strong :style="{ color: item.color }">{{ item.value }}项</strong>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- C 数据总览 -->
|
|
|
+ <section class="home-section">
|
|
|
+ <div class="section-heading"><el-icon><DataAnalysis /></el-icon><span>C · 数据总览</span></div>
|
|
|
+ <div class="metric-grid">
|
|
|
+ <div v-for="card in overviewCards" :key="card.key" class="metric">
|
|
|
+ <div class="metric-icon"><el-icon><component :is="overviewIcon(card.key)"/></el-icon></div>
|
|
|
+ <div class="metric-body">
|
|
|
+ <span>{{ card.label }}</span>
|
|
|
+ <div class="metric-value">
|
|
|
+ <strong :class="card.tone">{{ card.value }}</strong><small>{{ card.unit }}</small>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div ref="hazardChartRef" class="chart-box hazard-chart"></div>
|
|
|
- </section>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <a href="https://deerflow.tech" target="_blank" class="deerflow-mark" title="Created By Deerflow">
|
|
|
- <span class="deerflow-diamond">◆</span>
|
|
|
- <span class="deerflow-text">Deerflow</span>
|
|
|
- </a>
|
|
|
+ <div class="chart-grid">
|
|
|
+ <el-card shadow="never" class="water-panel chart-card trend-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-title">
|
|
|
+ <el-icon><TrendCharts /></el-icon><span>报警趋势</span>
|
|
|
+ <span class="muted">近 {{ trendDays }} 天 · 按四个报警等级分别统计</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div ref="trendChartRef" class="chart" />
|
|
|
+ </el-card>
|
|
|
+ <el-card shadow="never" class="water-panel chart-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-title"><el-icon><PieChart /></el-icon><span>报警分类占比</span></div>
|
|
|
+ </template>
|
|
|
+ <div ref="warningPieRef" class="chart" />
|
|
|
+ </el-card>
|
|
|
+ <el-card shadow="never" class="water-panel chart-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-title"><el-icon><DataAnalysis /></el-icon><span>设备分类占比</span></div>
|
|
|
+ </template>
|
|
|
+ <div ref="devicePieRef" class="chart" />
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
-<style scoped lang="scss">
|
|
|
-.gas-home-screen {
|
|
|
- min-height: 100vh;
|
|
|
- color: #fff;
|
|
|
- background:
|
|
|
- linear-gradient(180deg, rgba(14, 42, 70, 0.96), rgba(8, 20, 37, 1)),
|
|
|
- radial-gradient(circle at center, rgba(36, 122, 184, 0.18), transparent 58%),
|
|
|
- #081425;
|
|
|
- position: relative;
|
|
|
-}
|
|
|
-
|
|
|
-.gas-home-screen::before {
|
|
|
- content: '';
|
|
|
- position: absolute;
|
|
|
- inset: 0;
|
|
|
- pointer-events: none;
|
|
|
- z-index: 0;
|
|
|
- background:
|
|
|
- radial-gradient(circle at 28% 32%, rgba(56, 216, 255, 0.06) 0%, transparent 45%),
|
|
|
- radial-gradient(circle at 72% 58%, rgba(22, 109, 255, 0.05) 0%, transparent 40%),
|
|
|
- radial-gradient(circle at 50% 85%, rgba(56, 216, 255, 0.04) 0%, transparent 35%);
|
|
|
- animation: bgPulse 8s ease-in-out infinite alternate;
|
|
|
+<script setup>
|
|
|
+import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
|
+import { useRouter } from 'vue-router'
|
|
|
+import * as echarts from 'echarts'
|
|
|
+import {
|
|
|
+ Bell, BellFilled, Connection, Cpu, DataAnalysis, Finished, Grid, HomeFilled,
|
|
|
+ MapLocation, Monitor, OfficeBuilding, PieChart, Refresh, Tickets, TrendCharts,
|
|
|
+ Warning, WarningFilled
|
|
|
+} from '@element-plus/icons-vue'
|
|
|
+import { getAlarmLedgerDashboard, getAlarmStats, getDeviceGisAllPoints, getDeviceLedgerStatistics,
|
|
|
+ getWaterFacilityOverview, listAbnormalDevices, listRealtimeAlarms } from '@/api/pipeNetwork/waterSupply'
|
|
|
+import { listWorkOrder, workOrderStatusCount } from '@/api/pipeNetwork/ops/workOrder'
|
|
|
+import { normalizePage, unwrapResponse } from '@/utils/waterSupplyModel'
|
|
|
+import {
|
|
|
+ HOME_QUICK_ENTRIES, buildHomeWorkOrderQuery, buildOverviewMapPoints,
|
|
|
+ normalizeAbnormalTodoRows, normalizeAlarmOverviewCards, normalizeAlarmTrend, normalizeDeviceSummary,
|
|
|
+ normalizeHomeKpis, normalizePieRatio, normalizeRealtimeAlarmRows,
|
|
|
+ normalizeWorkOrderRows, normalizeWorkOrderStatusStat
|
|
|
+} from '@/utils/waterHomeModel'
|
|
|
+
|
|
|
+const router = useRouter()
|
|
|
+
|
|
|
+const loading = ref(true)
|
|
|
+const loadError = ref('')
|
|
|
+const trendDays = 7
|
|
|
+const quickEntries = HOME_QUICK_ENTRIES
|
|
|
+
|
|
|
+const facilityRows = ref([])
|
|
|
+const ledgerStats = ref({})
|
|
|
+const alarmStats = ref({})
|
|
|
+const alarmDashboard = ref({})
|
|
|
+const alarmRows = ref([])
|
|
|
+const workOrderRows = ref([])
|
|
|
+const workOrderStat = ref({})
|
|
|
+const abnormalTodoRows = ref([])
|
|
|
+const overviewMapPoints = ref([])
|
|
|
+const selectedDevice = ref(null)
|
|
|
+
|
|
|
+const homeKpis = ref([])
|
|
|
+const overviewCards = ref([])
|
|
|
+const trendData = ref({ dates: [], levels: [] })
|
|
|
+const warningRatio = ref([])
|
|
|
+const deviceRatio = ref([])
|
|
|
+
|
|
|
+const trendChartRef = ref(null)
|
|
|
+const warningPieRef = ref(null)
|
|
|
+const devicePieRef = ref(null)
|
|
|
+let trendChart = null
|
|
|
+let warningPie = null
|
|
|
+let devicePie = null
|
|
|
+
|
|
|
+const KPI_ICON = {
|
|
|
+ facility: OfficeBuilding,
|
|
|
+ device: Cpu,
|
|
|
+ online: Connection,
|
|
|
+ alarm: BellFilled
|
|
|
+}
|
|
|
+
|
|
|
+const OVERVIEW_ICON = {
|
|
|
+ alarmTotal: BellFilled,
|
|
|
+ alarmingDevice: WarningFilled,
|
|
|
+ verifyingOrder: Tickets,
|
|
|
+ onlineRate: TrendCharts
|
|
|
+}
|
|
|
+
|
|
|
+const ENTRY_ICON = {
|
|
|
+ abnormal: WarningFilled,
|
|
|
+ alarm: BellFilled,
|
|
|
+ audit: Finished,
|
|
|
+ map: MapLocation
|
|
|
+}
|
|
|
+
|
|
|
+function kpiIcon(key) { return KPI_ICON[key] || OfficeBuilding }
|
|
|
+function overviewIcon(key) { return OVERVIEW_ICON[key] || DataAnalysis }
|
|
|
+function entryIcon(key) { return ENTRY_ICON[key] || Grid }
|
|
|
+function levelType(level) {
|
|
|
+ const meta = { 1: 'danger', 2: 'warning', 3: 'primary', 4: 'success' }
|
|
|
+ return meta[Number(level)] || 'info'
|
|
|
+}
|
|
|
+function orderType(status) {
|
|
|
+ const meta = {
|
|
|
+ 0: 'danger', 1: 'warning', 2: 'primary', 3: 'warning',
|
|
|
+ 4: 'primary', 5: 'success', 6: 'warning', 7: 'info'
|
|
|
+ }
|
|
|
+ return meta[Number(status)] || 'info'
|
|
|
}
|
|
|
-
|
|
|
-@keyframes bgPulse {
|
|
|
- 0% { opacity: 0.6; }
|
|
|
- 100% { opacity: 1; }
|
|
|
+function dotByOrderType(type) {
|
|
|
+ const map = { danger: 'dot-danger', warning: 'dot-warning', primary: 'dot-primary', success: 'dot-success', info: 'dot-info' }
|
|
|
+ return map[type] || 'dot-info'
|
|
|
}
|
|
|
-
|
|
|
-.screen-header {
|
|
|
- height: 72px;
|
|
|
- padding: 0 24px;
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 320px 1fr 320px;
|
|
|
- align-items: center;
|
|
|
- position: relative;
|
|
|
- z-index: 1;
|
|
|
- background: linear-gradient(90deg, rgba(27, 98, 155, 0.95), rgba(18, 53, 98, 0.95));
|
|
|
- border-bottom: 1px solid rgba(93, 194, 255, 0.28);
|
|
|
+function abnormalDot(row) {
|
|
|
+ if (Number(row.alarmStatus) === 1) return 'dot-alarm'
|
|
|
+ if (Number(row.onlineStatus) === 0) return 'dot-offline'
|
|
|
+ return 'dot-warning'
|
|
|
}
|
|
|
-
|
|
|
-.screen-header::after {
|
|
|
- content: '';
|
|
|
- position: absolute;
|
|
|
- bottom: -1px;
|
|
|
- left: 5%;
|
|
|
- width: 90%;
|
|
|
- height: 1px;
|
|
|
- background: linear-gradient(90deg, transparent, rgba(56, 216, 255, 0.7), rgba(22, 109, 255, 0.7), transparent);
|
|
|
- animation: headerGlow 4s ease-in-out infinite alternate;
|
|
|
+function abnormalType(row) {
|
|
|
+ if (Number(row.alarmStatus) === 1) return 'danger'
|
|
|
+ if (Number(row.onlineStatus) === 0) return 'info'
|
|
|
+ return 'warning'
|
|
|
}
|
|
|
|
|
|
-@keyframes headerGlow {
|
|
|
- 0% { opacity: 0.4; }
|
|
|
- 100% { opacity: 1; }
|
|
|
+async function settle(fn, fallback = null) {
|
|
|
+ try {
|
|
|
+ return await fn()
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('供水首页数据片段加载失败', error)
|
|
|
+ if (fallback !== null) return fallback
|
|
|
+ throw error
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-.header-time {
|
|
|
- font-size: 18px;
|
|
|
- font-weight: 700;
|
|
|
- letter-spacing: 1.5px;
|
|
|
- color: #ddf6ff;
|
|
|
- font-variant-numeric: tabular-nums;
|
|
|
- text-shadow: 0 0 18px rgba(56, 216, 255, 0.35);
|
|
|
-}
|
|
|
+async function loadAll() {
|
|
|
+ loading.value = true
|
|
|
+ loadError.value = ''
|
|
|
+ const problems = []
|
|
|
+ const recordProblem = label => () => { problems.push(label) }
|
|
|
+
|
|
|
+ const [overview, ledger, alarm, dashboard, realtime, abnormal, orderStat, orders, deviceGis] = await Promise.allSettled([
|
|
|
+ settle(() => getWaterFacilityOverview(), []).catch(recordProblem('设施概览')),
|
|
|
+ settle(() => getDeviceLedgerStatistics(), {}).catch(recordProblem('设备统计')),
|
|
|
+ settle(() => getAlarmStats(), {}).catch(recordProblem('报警统计')),
|
|
|
+ settle(() => getAlarmLedgerDashboard(trendDays), {}).catch(recordProblem('报警态势')),
|
|
|
+ settle(() => listRealtimeAlarms(), []).catch(recordProblem('实时报警')),
|
|
|
+ settle(() => listAbnormalDevices({ pageNum: 1, pageSize: 8 }), { records: [], total: 0 }).catch(recordProblem('异常待办')),
|
|
|
+ settle(() => workOrderStatusCount(buildHomeWorkOrderQuery()), { total: 0, counts: [] }).catch(recordProblem('工单统计')),
|
|
|
+ settle(() => listWorkOrder(buildHomeWorkOrderQuery({ pageSize: 6 })), { records: [], total: 0 }).catch(recordProblem('工单列表')),
|
|
|
+ settle(() => getDeviceGisAllPoints(), { features: [] }).catch(recordProblem('设备点位'))
|
|
|
+ ])
|
|
|
+
|
|
|
+ const overviewData = unwrapResponse(overview.value || [])
|
|
|
+ facilityRows.value = Array.isArray(overviewData) ? overviewData : (overviewData?.facilityOverview || [])
|
|
|
+ ledgerStats.value = unwrapResponse(ledger.value || {})
|
|
|
+ alarmStats.value = unwrapResponse(alarm.value || {})
|
|
|
+ alarmDashboard.value = unwrapResponse(dashboard.value || {})
|
|
|
+ alarmRows.value = normalizeRealtimeAlarmRows(unwrapResponse(realtime.value || []), 6)
|
|
|
+ const abnormalPage = normalizePage(abnormal.value || { records: [], total: 0 })
|
|
|
+ abnormalTodoRows.value = normalizeAbnormalTodoRows(abnormalPage.records, 7)
|
|
|
+ workOrderStat.value = normalizeWorkOrderStatusStat(unwrapResponse(orderStat.value || { total: 0, counts: [] }))
|
|
|
+ const orderList = unwrapResponse(orders.value || { records: [] })
|
|
|
+ workOrderRows.value = normalizeWorkOrderRows(orderList?.records || [], 6)
|
|
|
+
|
|
|
+ const gisData = unwrapResponse(deviceGis.value || {})
|
|
|
+ const devicePoints = Array.isArray(gisData) ? gisData : (gisData?.features || [])
|
|
|
+ const alarmSource = normalizeRealtimeAlarmRows(unwrapResponse(realtime.value || []), 50)
|
|
|
+ overviewMapPoints.value = buildOverviewMapPoints({ devicePoints, alarmRows: alarmSource })
|
|
|
+
|
|
|
+ homeKpis.value = normalizeHomeKpis({ facilityRows: facilityRows.value, ledgerStats: ledgerStats.value, alarmStats: alarmStats.value })
|
|
|
+ overviewCards.value = normalizeAlarmOverviewCards({
|
|
|
+ dashboard: alarmDashboard.value,
|
|
|
+ deviceStats: ledgerStats.value,
|
|
|
+ workOrderStat: workOrderStat.value
|
|
|
+ })
|
|
|
+ trendData.value = normalizeAlarmTrend(alarmDashboard.value?.trend || [])
|
|
|
+ warningRatio.value = normalizePieRatio(alarmDashboard.value?.warningTypeRatio || [], '未分类', 6)
|
|
|
+ deviceRatio.value = normalizePieRatio(alarmDashboard.value?.deviceTypeRatio || [], '未分类', 6)
|
|
|
|
|
|
-.header-title-wrap {
|
|
|
- text-align: center;
|
|
|
+ if (problems.length) loadError.value = problems.join(';')
|
|
|
+ loading.value = false
|
|
|
+ await nextTick()
|
|
|
+ renderCharts()
|
|
|
}
|
|
|
|
|
|
-.header-title {
|
|
|
- font-size: 30px;
|
|
|
- font-weight: 800;
|
|
|
- letter-spacing: 3px;
|
|
|
- background: linear-gradient(180deg, #e8f6ff 0%, #8fd6ff 100%);
|
|
|
- -webkit-background-clip: text;
|
|
|
- -webkit-text-fill-color: transparent;
|
|
|
- background-clip: text;
|
|
|
+function selectAbnormal(row) {
|
|
|
+ selectedDevice.value = normalizeDeviceSummary(row)
|
|
|
}
|
|
|
|
|
|
-.header-subtitle {
|
|
|
- margin-top: 4px;
|
|
|
- color: rgba(196, 234, 255, 0.65);
|
|
|
- font-size: 11px;
|
|
|
- letter-spacing: 3px;
|
|
|
- text-transform: uppercase;
|
|
|
+function resolveRoutePath(suffix) {
|
|
|
+ const matched = router.getRoutes().find(item => item.path.endsWith(suffix))
|
|
|
+ return matched ? matched.path : ''
|
|
|
}
|
|
|
|
|
|
-.header-tags {
|
|
|
- display: flex;
|
|
|
- justify-content: flex-end;
|
|
|
- gap: 8px;
|
|
|
+function goPage(suffix, query = {}) {
|
|
|
+ const path = resolveRoutePath(suffix)
|
|
|
+ if (!path) {
|
|
|
+ console.warn(`未找到供水入口路由: ${suffix}`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ router.push({ path, query }).catch(() => {})
|
|
|
}
|
|
|
|
|
|
-.header-tag {
|
|
|
- padding: 6px 14px;
|
|
|
- border: 1px solid rgba(117, 206, 255, 0.35);
|
|
|
- border-radius: 16px;
|
|
|
- color: #d8f5ff;
|
|
|
- background: rgba(13, 77, 128, 0.45);
|
|
|
- font-size: 12px;
|
|
|
- letter-spacing: 1px;
|
|
|
- transition: all 0.3s;
|
|
|
- cursor: default;
|
|
|
+function renderCharts() {
|
|
|
+ renderTrendChart()
|
|
|
+ warningPie?.dispose()
|
|
|
+ warningPie = renderPieChart(warningPieRef.value, warningRatio.value, '报警数')
|
|
|
+ devicePie?.dispose()
|
|
|
+ devicePie = renderPieChart(devicePieRef.value, deviceRatio.value, '设备数')
|
|
|
}
|
|
|
|
|
|
-.header-tag:hover {
|
|
|
- border-color: rgba(117, 206, 255, 0.7);
|
|
|
- background: rgba(13, 77, 128, 0.65);
|
|
|
- box-shadow: 0 0 16px rgba(56, 216, 255, 0.15);
|
|
|
+function renderTrendChart() {
|
|
|
+ if (!trendChartRef.value) return
|
|
|
+ trendChart?.dispose()
|
|
|
+ trendChart = echarts.init(trendChartRef.value)
|
|
|
+ const { dates, levels } = trendData.value
|
|
|
+ const palette = ['#f56c6c', '#e6a23c', '#409eff', '#67c23a']
|
|
|
+ trendChart.setOption({
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ legend: { bottom: 0, icon: 'circle', itemWidth: 9, itemHeight: 9, textStyle: { color: '#475569' } },
|
|
|
+ grid: { left: 34, right: 14, top: 18, bottom: 48, containLabel: true },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: dates,
|
|
|
+ axisLine: { lineStyle: { color: '#cbd5e1' } },
|
|
|
+ axisLabel: { color: '#64748b' }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ minInterval: 1,
|
|
|
+ splitLine: { lineStyle: { color: '#eef2f7' } },
|
|
|
+ axisLabel: { color: '#64748b' }
|
|
|
+ },
|
|
|
+ color: palette,
|
|
|
+ series: levels.map((level, index) => ({
|
|
|
+ name: level.text || `级${index + 1}`,
|
|
|
+ type: 'bar',
|
|
|
+ stack: 'alarm',
|
|
|
+ barWidth: 26,
|
|
|
+ data: level.data,
|
|
|
+ itemStyle: { borderRadius: index === levels.length - 1 ? [5, 5, 0, 0] : 0 }
|
|
|
+ }))
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
-.screen-body {
|
|
|
- height: calc(100vh - 72px);
|
|
|
- padding: 12px;
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 360px 1fr 360px;
|
|
|
- gap: 12px;
|
|
|
- position: relative;
|
|
|
- z-index: 1;
|
|
|
+function renderPieChart(container, data, unit) {
|
|
|
+ if (!container) return null
|
|
|
+ const total = data.reduce((sum, item) => sum + item.count, 0)
|
|
|
+ const chart = echarts.init(container)
|
|
|
+ chart.setOption({
|
|
|
+ tooltip: { trigger: 'item', formatter: `{b}:{c} ${unit}({d}%)` },
|
|
|
+ legend: { bottom: 0, icon: 'circle', itemWidth: 9, itemHeight: 9, textStyle: { color: '#475569' } },
|
|
|
+ title: {
|
|
|
+ text: String(total),
|
|
|
+ subtext: unit,
|
|
|
+ left: 'center',
|
|
|
+ top: '32%',
|
|
|
+ textStyle: { color: '#1e293b', fontSize: 24, fontWeight: 700 },
|
|
|
+ subtextStyle: { color: '#64748b', fontSize: 12 }
|
|
|
+ },
|
|
|
+ color: ['#2563eb', '#14b8a6', '#f59e0b', '#ef4444', '#8b5cf6', '#64748b'],
|
|
|
+ series: [{
|
|
|
+ type: 'pie',
|
|
|
+ radius: ['48%', '68%'],
|
|
|
+ center: ['50%', '43%'],
|
|
|
+ data: data.map(item => ({ name: item.name, value: item.count })),
|
|
|
+ label: { formatter: '{b}\n{d}%', color: '#475569', lineHeight: 17 },
|
|
|
+ itemStyle: { borderColor: '#fff', borderWidth: 3, borderRadius: 8 },
|
|
|
+ emphasis: { scaleSize: 7 }
|
|
|
+ }]
|
|
|
+ })
|
|
|
+ return chart
|
|
|
}
|
|
|
|
|
|
-.side-panel,
|
|
|
-.center-panel {
|
|
|
- min-height: 0;
|
|
|
+function handleResize() {
|
|
|
+ trendChart?.resize()
|
|
|
+ warningPie?.resize()
|
|
|
+ devicePie?.resize()
|
|
|
}
|
|
|
|
|
|
-.left-panel,
|
|
|
-.right-panel {
|
|
|
- display: grid;
|
|
|
- gap: 12px;
|
|
|
-}
|
|
|
+onMounted(async () => {
|
|
|
+ await loadAll()
|
|
|
+ window.addEventListener('resize', handleResize)
|
|
|
+})
|
|
|
|
|
|
-.left-panel {
|
|
|
- grid-template-rows: 220px 220px 1fr;
|
|
|
-}
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ window.removeEventListener('resize', handleResize)
|
|
|
+ trendChart?.dispose()
|
|
|
+ warningPie?.dispose()
|
|
|
+ devicePie?.dispose()
|
|
|
+})
|
|
|
+</script>
|
|
|
|
|
|
-.right-panel {
|
|
|
- grid-template-rows: 250px 260px 1fr;
|
|
|
-}
|
|
|
+<style lang="scss">
|
|
|
+@use './waterSupplyPageTheme.scss';
|
|
|
+</style>
|
|
|
|
|
|
-.panel-card {
|
|
|
- min-height: 0;
|
|
|
- padding: 14px;
|
|
|
- position: relative;
|
|
|
- background: linear-gradient(180deg, rgba(13, 33, 56, 0.88), rgba(10, 25, 42, 0.88));
|
|
|
- border: 1px solid rgba(65, 158, 219, 0.22);
|
|
|
- border-radius: 12px;
|
|
|
- box-shadow: 0 2px 24px rgba(0, 0, 0, 0.25), inset 0 0 0 1px rgba(255, 255, 255, 0.025);
|
|
|
- transition: border-color 0.4s;
|
|
|
+<style scoped>
|
|
|
+.home-page {
|
|
|
+ background: linear-gradient(135deg, #eff6ff 0%, #f8fafc 45%, #eef2f7 100%);
|
|
|
}
|
|
|
|
|
|
-.panel-card::after {
|
|
|
- content: '';
|
|
|
- position: absolute;
|
|
|
- top: -1px;
|
|
|
- left: 20px;
|
|
|
- right: 20px;
|
|
|
- height: 1px;
|
|
|
- border-radius: 1px;
|
|
|
- background: linear-gradient(90deg, transparent, rgba(56, 216, 255, 0.25), transparent);
|
|
|
- pointer-events: none;
|
|
|
+.heading-actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
}
|
|
|
|
|
|
-.panel-card:hover {
|
|
|
- border-color: rgba(65, 158, 219, 0.38);
|
|
|
+.page-title-icon {
|
|
|
+ display: grid;
|
|
|
+ place-items: center;
|
|
|
+ width: 42px;
|
|
|
+ height: 42px;
|
|
|
+ color: #fff;
|
|
|
+ background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 8px 16px rgba(37, 99, 235, 0.2);
|
|
|
}
|
|
|
|
|
|
-.panel-title {
|
|
|
- position: relative;
|
|
|
- padding-left: 16px;
|
|
|
+.load-error {
|
|
|
margin-bottom: 14px;
|
|
|
- color: #8fe5ff;
|
|
|
- font-size: 18px;
|
|
|
- font-weight: 800;
|
|
|
- letter-spacing: 1px;
|
|
|
}
|
|
|
|
|
|
-.panel-title::before {
|
|
|
- content: '';
|
|
|
- position: absolute;
|
|
|
- left: 0;
|
|
|
- top: 3px;
|
|
|
- width: 4px;
|
|
|
- height: 20px;
|
|
|
- border-radius: 2px;
|
|
|
- background: linear-gradient(180deg, #28d7ff, #1776ff);
|
|
|
- box-shadow: 0 0 10px rgba(40, 215, 255, 0.5);
|
|
|
+.home-section {
|
|
|
+ margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
-.base-grid {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
- gap: 14px;
|
|
|
+.section-heading {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ color: #17324d;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 700;
|
|
|
}
|
|
|
|
|
|
-.base-card {
|
|
|
- background: linear-gradient(180deg, rgba(10, 30, 53, 0.95), rgba(9, 24, 42, 0.95));
|
|
|
+.section-heading :deep(.el-icon) {
|
|
|
+ color: #2563eb;
|
|
|
+ font-size: 18px;
|
|
|
}
|
|
|
|
|
|
-.base-grid {
|
|
|
- grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
+.metric-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
gap: 12px;
|
|
|
+ margin-bottom: 14px;
|
|
|
}
|
|
|
|
|
|
-.base-item {
|
|
|
+.metric {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 14px;
|
|
|
- min-height: 72px;
|
|
|
- padding: 14px;
|
|
|
- border-radius: 14px;
|
|
|
- background: linear-gradient(180deg, rgba(20, 57, 89, 0.45), rgba(18, 44, 72, 0.35));
|
|
|
- border: 1px solid rgba(97, 190, 255, 0.12);
|
|
|
- transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
- cursor: default;
|
|
|
- position: relative;
|
|
|
- overflow: hidden;
|
|
|
+ min-width: 0;
|
|
|
+ padding: 14px 16px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #e1ebf3;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 6px 14px rgba(15, 23, 42, 0.04);
|
|
|
}
|
|
|
|
|
|
-.base-item::before {
|
|
|
- content: '';
|
|
|
- position: absolute;
|
|
|
- inset: 0;
|
|
|
- border-radius: 14px;
|
|
|
- background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.03), transparent 70%);
|
|
|
- pointer-events: none;
|
|
|
- transition: opacity 0.35s;
|
|
|
- opacity: 0;
|
|
|
+.metric-icon {
|
|
|
+ position: static;
|
|
|
+ flex: 0 0 42px;
|
|
|
+ display: grid;
|
|
|
+ place-items: center;
|
|
|
+ width: 42px;
|
|
|
+ height: 42px;
|
|
|
+ color: #2563eb;
|
|
|
+ background: linear-gradient(135deg, #dbeafe 0%, #eff6ff 100%);
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 22px;
|
|
|
}
|
|
|
|
|
|
-.base-item:hover {
|
|
|
- transform: translateY(-2px);
|
|
|
- border-color: rgba(97, 190, 255, 0.35);
|
|
|
- box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(97, 190, 255, 0.1) inset;
|
|
|
+.metric-body {
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
|
|
|
-.base-item:hover::before {
|
|
|
- opacity: 1;
|
|
|
+.metric-body span {
|
|
|
+ display: block;
|
|
|
+ overflow: hidden;
|
|
|
+ margin-bottom: 3px;
|
|
|
+ color: #64748b;
|
|
|
+ font-size: 13px;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
-.base-icon {
|
|
|
- width: 48px;
|
|
|
- height: 48px;
|
|
|
- border-radius: 16px;
|
|
|
+.metric-value {
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- flex: 0 0 48px;
|
|
|
- position: relative;
|
|
|
- transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.35s;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 5px;
|
|
|
}
|
|
|
|
|
|
-.base-item:hover .base-icon {
|
|
|
- transform: scale(1.08);
|
|
|
- box-shadow: 0 0 28px rgba(56, 216, 255, 0.3);
|
|
|
+.metric-value strong {
|
|
|
+ color: #1e293b;
|
|
|
+ font-size: 24px;
|
|
|
+ line-height: 1.2;
|
|
|
}
|
|
|
|
|
|
-.base-icon-svg {
|
|
|
- width: 26px;
|
|
|
- height: 26px;
|
|
|
- color: #fff;
|
|
|
- transition: transform 0.35s;
|
|
|
+.metric-value strong.danger { color: #dc2626; }
|
|
|
+.metric-value strong.warning { color: #d97706; }
|
|
|
+
|
|
|
+.metric-value small {
|
|
|
+ color: #94a3b8;
|
|
|
}
|
|
|
|
|
|
-.base-item:hover .base-icon-svg {
|
|
|
- transform: scale(1.1);
|
|
|
+.workbench-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
+ gap: 12px;
|
|
|
}
|
|
|
|
|
|
-.base-text {
|
|
|
+.home-panel {
|
|
|
min-width: 0;
|
|
|
+ border: 1px solid #e1ebf3;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 5px 16px rgba(28, 75, 110, 0.05);
|
|
|
}
|
|
|
|
|
|
-.base-label {
|
|
|
- color: #c0dff5;
|
|
|
- font-size: 12px;
|
|
|
- letter-spacing: 1px;
|
|
|
- text-transform: uppercase;
|
|
|
+.home-panel :deep(.el-card__header) {
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-bottom: 1px solid #e1ebf3;
|
|
|
+ background: #fbfdff;
|
|
|
}
|
|
|
|
|
|
-.base-value {
|
|
|
- margin-top: 4px;
|
|
|
- color: #fff;
|
|
|
- font-size: 32px;
|
|
|
- font-weight: 800;
|
|
|
- line-height: 1;
|
|
|
- font-variant-numeric: tabular-nums;
|
|
|
- letter-spacing: -0.5px;
|
|
|
+.home-panel :deep(.el-card__body) {
|
|
|
+ padding: 12px 14px;
|
|
|
}
|
|
|
|
|
|
-.base-value span {
|
|
|
- margin-left: 4px;
|
|
|
- color: #90d9ff;
|
|
|
- font-size: 12px;
|
|
|
- font-weight: 500;
|
|
|
- letter-spacing: 0;
|
|
|
+.card-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ color: #1e293b;
|
|
|
+ font-weight: 600;
|
|
|
}
|
|
|
|
|
|
-.chart-box {
|
|
|
- width: 100%;
|
|
|
- height: calc(100% - 36px);
|
|
|
+.card-title :deep(.el-icon) {
|
|
|
+ color: #2563eb;
|
|
|
+ font-size: 18px;
|
|
|
}
|
|
|
|
|
|
-.table-card,
|
|
|
-.warning-card,
|
|
|
-.hazard-card {
|
|
|
- overflow: hidden;
|
|
|
+.card-title .muted {
|
|
|
+ margin-left: auto;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 400;
|
|
|
}
|
|
|
|
|
|
-.table-wrap {
|
|
|
- height: calc(100% - 38px);
|
|
|
- overflow: auto;
|
|
|
+.panel-link {
|
|
|
+ margin-left: auto;
|
|
|
}
|
|
|
|
|
|
-.data-table {
|
|
|
- width: 100%;
|
|
|
- border-collapse: collapse;
|
|
|
- table-layout: fixed;
|
|
|
+.compact-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 7px;
|
|
|
+ min-height: 220px;
|
|
|
}
|
|
|
|
|
|
-.data-table thead {
|
|
|
- background: rgba(24, 122, 198, 0.55);
|
|
|
+.compact-item {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 10px minmax(0, 1fr) auto;
|
|
|
+ gap: 9px;
|
|
|
+ align-items: center;
|
|
|
+ width: 100%;
|
|
|
+ padding: 9px 10px;
|
|
|
+ text-align: left;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #eef2f7;
|
|
|
+ border-radius: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.15s ease;
|
|
|
}
|
|
|
|
|
|
-.data-table thead th {
|
|
|
- font-weight: 700;
|
|
|
- letter-spacing: 0.5px;
|
|
|
- color: #b9e4ff;
|
|
|
+.compact-item:hover {
|
|
|
+ background: #f6fbff;
|
|
|
+ border-color: #bfdbfe;
|
|
|
}
|
|
|
|
|
|
-.data-table th,
|
|
|
-.data-table td {
|
|
|
- padding: 10px 6px;
|
|
|
- color: #f4fbff;
|
|
|
- text-align: center;
|
|
|
- font-size: 13px;
|
|
|
- border-bottom: 1px solid rgba(130, 194, 231, 0.1);
|
|
|
- white-space: nowrap;
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
+.compact-item.active {
|
|
|
+ background: #eef6ff;
|
|
|
+ border-color: #93c5fd;
|
|
|
}
|
|
|
|
|
|
-.data-table tbody tr {
|
|
|
- transition: background 0.25s;
|
|
|
+.status-dot {
|
|
|
+ width: 9px;
|
|
|
+ height: 9px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #94a3b8;
|
|
|
}
|
|
|
|
|
|
-.data-table tbody tr:nth-child(odd) {
|
|
|
- background: rgba(133, 196, 242, 0.10);
|
|
|
+.dot-alarm { background: #dc2626; }
|
|
|
+.dot-offline { background: #94a3b8; }
|
|
|
+.dot-normal { background: #16a34a; }
|
|
|
+.dot-warning { background: #d97706; }
|
|
|
+.dot-danger { background: #dc2626; }
|
|
|
+.dot-primary { background: #2563eb; }
|
|
|
+.dot-success { background: #16a34a; }
|
|
|
+.dot-info { background: #94a3b8; }
|
|
|
+
|
|
|
+.compact-main {
|
|
|
+ display: block;
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
|
|
|
-.data-table tbody tr:nth-child(even) {
|
|
|
- background: rgba(133, 196, 242, 0.04);
|
|
|
+.compact-main b,
|
|
|
+.compact-main small {
|
|
|
+ display: block;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
-.data-table tbody tr:hover {
|
|
|
- background: rgba(56, 216, 255, 0.12);
|
|
|
+.compact-main b {
|
|
|
+ color: #1e293b;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
}
|
|
|
|
|
|
-.compact-table th,
|
|
|
-.compact-table td {
|
|
|
- padding: 9px 4px;
|
|
|
+.compact-main small {
|
|
|
+ margin-top: 2px;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
|
|
|
-.status-cell.ok {
|
|
|
- color: #6effc4;
|
|
|
- position: relative;
|
|
|
+.compact-meta {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-end;
|
|
|
+ gap: 3px;
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
|
|
|
-.status-cell.warn {
|
|
|
- color: #ffe16a;
|
|
|
+.compact-meta small {
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 11px;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
-.status-cell.off {
|
|
|
- color: #ff8e8e;
|
|
|
+.map-legend {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 7px;
|
|
|
+ margin-left: auto;
|
|
|
+ color: #64748b;
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
|
|
|
-.status-cell::before {
|
|
|
- content: '';
|
|
|
+.legend-dot {
|
|
|
display: inline-block;
|
|
|
- width: 6px;
|
|
|
- height: 6px;
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
border-radius: 50%;
|
|
|
- margin-right: 6px;
|
|
|
- vertical-align: middle;
|
|
|
-}
|
|
|
-
|
|
|
-.status-cell.ok::before {
|
|
|
- background: #6effc4;
|
|
|
- box-shadow: 0 0 6px rgba(110, 255, 196, 0.6);
|
|
|
-}
|
|
|
-
|
|
|
-.status-cell.warn::before {
|
|
|
- background: #ffe16a;
|
|
|
- box-shadow: 0 0 6px rgba(255, 225, 106, 0.6);
|
|
|
-}
|
|
|
-
|
|
|
-.status-cell.off::before {
|
|
|
- background: #ff8e8e;
|
|
|
- box-shadow: 0 0 6px rgba(255, 142, 142, 0.6);
|
|
|
-}
|
|
|
-
|
|
|
-.center-panel {
|
|
|
- position: relative;
|
|
|
}
|
|
|
|
|
|
.map-stage {
|
|
|
position: relative;
|
|
|
- height: 100%;
|
|
|
+ min-height: 226px;
|
|
|
overflow: hidden;
|
|
|
- border-radius: 12px;
|
|
|
- border: 1px solid rgba(56, 159, 223, 0.2);
|
|
|
- background: rgba(8, 20, 37, 0.64);
|
|
|
- box-shadow: 0 0 40px rgba(0, 0, 0, 0.3), inset 0 0 60px rgba(8, 30, 60, 0.3);
|
|
|
+ border: 1px solid #e1ebf3;
|
|
|
+ border-radius: 8px;
|
|
|
+ background:
|
|
|
+ linear-gradient(rgba(219, 234, 254, 0.35) 1px, transparent 1px),
|
|
|
+ linear-gradient(90deg, rgba(219, 234, 254, 0.35) 1px, transparent 1px),
|
|
|
+ linear-gradient(135deg, #eef6ff 0%, #f8fbff 100%);
|
|
|
+ background-size: 28px 28px;
|
|
|
}
|
|
|
|
|
|
-.map-stage-top {
|
|
|
+.map-point {
|
|
|
position: absolute;
|
|
|
- top: 14px;
|
|
|
- left: 14px;
|
|
|
- right: 14px;
|
|
|
- z-index: 3;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ z-index: 2;
|
|
|
display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- gap: 12px;
|
|
|
- pointer-events: none;
|
|
|
-}
|
|
|
-
|
|
|
-.summary-strip,
|
|
|
-.map-kpis {
|
|
|
- display: flex;
|
|
|
- gap: 10px;
|
|
|
-}
|
|
|
-
|
|
|
-.summary-item,
|
|
|
-.map-kpi {
|
|
|
- min-width: 110px;
|
|
|
- padding: 10px 14px;
|
|
|
- border-radius: 10px;
|
|
|
- background: rgba(8, 31, 54, 0.82);
|
|
|
- border: 1px solid rgba(79, 177, 238, 0.18);
|
|
|
- backdrop-filter: blur(8px);
|
|
|
- transition: all 0.3s;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 3px;
|
|
|
+ cursor: default;
|
|
|
}
|
|
|
|
|
|
-.summary-label,
|
|
|
-.map-kpi span {
|
|
|
+.map-point span {
|
|
|
display: block;
|
|
|
- color: #91d7ff;
|
|
|
- font-size: 12px;
|
|
|
- letter-spacing: 0.5px;
|
|
|
+ width: 12px;
|
|
|
+ height: 12px;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: 2px solid #fff;
|
|
|
+ box-shadow: 0 1px 4px rgba(15, 23, 42, 0.28);
|
|
|
}
|
|
|
|
|
|
-.summary-value,
|
|
|
-.map-kpi strong {
|
|
|
- display: block;
|
|
|
- margin-top: 4px;
|
|
|
- color: #fff;
|
|
|
- font-size: 22px;
|
|
|
- font-weight: 800;
|
|
|
- letter-spacing: 1px;
|
|
|
+.map-point.state-alarm span {
|
|
|
+ background: #dc2626;
|
|
|
+ box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.16);
|
|
|
}
|
|
|
|
|
|
-.map-view {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
-}
|
|
|
+.map-point.state-offline span { background: #94a3b8; }
|
|
|
+.map-point.state-normal span { background: #16a34a; }
|
|
|
|
|
|
-.map-error {
|
|
|
- position: absolute;
|
|
|
- inset: 0;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- color: #ffd3d3;
|
|
|
- background: rgba(5, 10, 20, 0.72);
|
|
|
- z-index: 4;
|
|
|
+.map-point b {
|
|
|
+ max-width: 110px;
|
|
|
+ overflow: hidden;
|
|
|
+ padding: 1px 5px;
|
|
|
+ color: #334155;
|
|
|
+ background: rgba(255, 255, 255, 0.88);
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 10px;
|
|
|
+ font-weight: 600;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
-.map-panel {
|
|
|
+.map-note {
|
|
|
position: absolute;
|
|
|
- z-index: 3;
|
|
|
- width: 190px;
|
|
|
- padding: 14px;
|
|
|
- border-radius: 12px;
|
|
|
- background: rgba(8, 31, 54, 0.82);
|
|
|
- border: 1px solid rgba(79, 177, 238, 0.18);
|
|
|
- backdrop-filter: blur(10px);
|
|
|
+ left: 10px;
|
|
|
+ bottom: 8px;
|
|
|
+ z-index: 1;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 11px;
|
|
|
}
|
|
|
|
|
|
-.info-panel {
|
|
|
- left: 18px;
|
|
|
- bottom: 20px;
|
|
|
+.order-brief {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
+ gap: 6px;
|
|
|
+ margin-top: 10px;
|
|
|
+ padding-top: 10px;
|
|
|
+ border-top: 1px solid #eef2f7;
|
|
|
}
|
|
|
|
|
|
-.point-panel {
|
|
|
- right: 18px;
|
|
|
- bottom: 20px;
|
|
|
+.order-brief span {
|
|
|
+ color: #64748b;
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
|
|
|
-.mini-title {
|
|
|
- margin-bottom: 10px;
|
|
|
- color: #8fe5ff;
|
|
|
+.order-brief b {
|
|
|
+ margin-left: 3px;
|
|
|
+ color: #17324d;
|
|
|
font-size: 14px;
|
|
|
- font-weight: 700;
|
|
|
- letter-spacing: 0.5px;
|
|
|
}
|
|
|
|
|
|
-.mini-row {
|
|
|
+.device-summary {
|
|
|
display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- margin-top: 6px;
|
|
|
- padding: 4px 0;
|
|
|
- color: #d6efff;
|
|
|
- font-size: 13px;
|
|
|
- border-bottom: 1px solid rgba(130, 194, 231, 0.06);
|
|
|
-}
|
|
|
-
|
|
|
-.mini-row:last-child {
|
|
|
- border-bottom: none;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
}
|
|
|
|
|
|
-.warning-scroll {
|
|
|
- height: calc(100% - 38px);
|
|
|
- overflow-y: auto;
|
|
|
- padding-right: 4px;
|
|
|
+.summary-top {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 10px;
|
|
|
+ flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
-.warning-scroll::-webkit-scrollbar {
|
|
|
- width: 5px;
|
|
|
+.summary-name {
|
|
|
+ color: #17324d;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 700;
|
|
|
}
|
|
|
|
|
|
-.warning-scroll::-webkit-scrollbar-thumb {
|
|
|
- border-radius: 999px;
|
|
|
- background: rgba(96, 180, 235, 0.3);
|
|
|
+.summary-tags {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 6px;
|
|
|
}
|
|
|
|
|
|
-.warning-scroll::-webkit-scrollbar-track {
|
|
|
- background: transparent;
|
|
|
+.detail-panel :deep(.el-descriptions__label) {
|
|
|
+ width: 86px;
|
|
|
+ color: #64748b;
|
|
|
+ background: #f8fafc;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
-.warning-list {
|
|
|
+.quick-grid {
|
|
|
display: grid;
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
-.warning-item {
|
|
|
- display: flex;
|
|
|
- gap: 12px;
|
|
|
- align-items: stretch;
|
|
|
- padding: 14px;
|
|
|
- border-radius: 14px;
|
|
|
- border: 1px solid rgba(255, 108, 108, 0.1);
|
|
|
- background: linear-gradient(180deg, rgba(72, 24, 38, 0.42), rgba(49, 19, 31, 0.30));
|
|
|
- transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
- position: relative;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-item::before {
|
|
|
- content: '';
|
|
|
- position: absolute;
|
|
|
- left: 0;
|
|
|
- top: 12px;
|
|
|
- bottom: 12px;
|
|
|
- width: 3px;
|
|
|
- border-radius: 0 3px 3px 0;
|
|
|
- transition: all 0.35s;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-item.danger::before {
|
|
|
- background: linear-gradient(180deg, #ff5858, #ff3030);
|
|
|
- box-shadow: 0 0 12px rgba(255, 58, 58, 0.5);
|
|
|
- animation: dangerPulse 2s ease-in-out infinite;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-item.notice::before {
|
|
|
- background: linear-gradient(180deg, #ffc635, #ff9f1a);
|
|
|
- box-shadow: 0 0 10px rgba(255, 198, 53, 0.35);
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes dangerPulse {
|
|
|
- 0%, 100% { opacity: 0.6; }
|
|
|
- 50% { opacity: 1; }
|
|
|
-}
|
|
|
-
|
|
|
-.warning-item:hover {
|
|
|
- transform: translateX(3px);
|
|
|
-}
|
|
|
-
|
|
|
-.warning-item.danger:hover {
|
|
|
- border-color: rgba(255, 108, 108, 0.25);
|
|
|
- box-shadow: 0 4px 20px rgba(255, 58, 58, 0.1);
|
|
|
-}
|
|
|
-
|
|
|
-.warning-item.notice:hover {
|
|
|
- border-color: rgba(255, 201, 92, 0.2);
|
|
|
- box-shadow: 0 4px 20px rgba(255, 198, 53, 0.08);
|
|
|
-}
|
|
|
-
|
|
|
-.warning-item.notice {
|
|
|
- border-color: rgba(255, 201, 92, 0.1);
|
|
|
- background: linear-gradient(180deg, rgba(78, 63, 23, 0.28), rgba(48, 43, 20, 0.18));
|
|
|
-}
|
|
|
-
|
|
|
-.warning-side {
|
|
|
- width: 52px;
|
|
|
- flex: 0 0 52px;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 8px;
|
|
|
- align-items: center;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-badge {
|
|
|
- width: 34px;
|
|
|
- height: 34px;
|
|
|
- border-radius: 10px;
|
|
|
+.quick-entry {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- justify-content: center;
|
|
|
- font-size: 13px;
|
|
|
- font-weight: 800;
|
|
|
- color: #fff;
|
|
|
- transition: transform 0.3s;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-item:hover .warning-badge {
|
|
|
- transform: scale(1.1);
|
|
|
-}
|
|
|
-
|
|
|
-.warning-tag {
|
|
|
- width: 100%;
|
|
|
- padding: 3px 0;
|
|
|
- border-radius: 999px;
|
|
|
- text-align: center;
|
|
|
- font-size: 10px;
|
|
|
- letter-spacing: 0.5px;
|
|
|
- color: #dcefff;
|
|
|
- background: rgba(255, 255, 255, 0.06);
|
|
|
-}
|
|
|
-
|
|
|
-.warning-item.danger .warning-badge {
|
|
|
- background: rgba(255, 58, 58, 0.28);
|
|
|
- color: #ff8787;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-item.danger .warning-title {
|
|
|
- color: #ffd8d8;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-item.notice .warning-badge {
|
|
|
- background: rgba(255, 198, 53, 0.2);
|
|
|
- color: #ffd45d;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-item.notice .warning-title {
|
|
|
- color: #fff2c4;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-main {
|
|
|
+ gap: 10px;
|
|
|
min-width: 0;
|
|
|
- flex: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-title-row {
|
|
|
- display: flex;
|
|
|
- align-items: flex-start;
|
|
|
- justify-content: space-between;
|
|
|
- gap: 8px;
|
|
|
+ padding: 14px 12px;
|
|
|
+ color: #1e293b;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #e1ebf3;
|
|
|
+ border-radius: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.15s ease;
|
|
|
}
|
|
|
|
|
|
-.warning-title {
|
|
|
- font-weight: 700;
|
|
|
- font-size: 16px;
|
|
|
- line-height: 1.2;
|
|
|
+.quick-entry:hover {
|
|
|
+ background: #f6fbff;
|
|
|
+ border-color: #bfdbfe;
|
|
|
}
|
|
|
|
|
|
-.warning-time,
|
|
|
-.warning-code,
|
|
|
-.warning-desc {
|
|
|
- color: #dcefff;
|
|
|
- font-size: 12px;
|
|
|
+.quick-entry :deep(.el-icon) {
|
|
|
+ flex: 0 0 20px;
|
|
|
+ color: #2563eb;
|
|
|
+ font-size: 20px;
|
|
|
}
|
|
|
|
|
|
-.warning-time {
|
|
|
- color: #b6dfff;
|
|
|
+.quick-entry span {
|
|
|
+ overflow: hidden;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
-.warning-code {
|
|
|
- margin: 6px 0 4px;
|
|
|
- color: #9ad8ff;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-desc {
|
|
|
- line-height: 1.5;
|
|
|
-}
|
|
|
-
|
|
|
-.hazard-summary {
|
|
|
- display: flex;
|
|
|
+.chart-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1.5fr 1fr 1fr;
|
|
|
gap: 12px;
|
|
|
- margin-bottom: 10px;
|
|
|
}
|
|
|
|
|
|
-.hazard-summary.three-col {
|
|
|
- justify-content: space-between;
|
|
|
+.chart-card {
|
|
|
+ border: 1px solid #e1ebf3;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 5px 16px rgba(28, 75, 110, 0.05);
|
|
|
}
|
|
|
|
|
|
-.hazard-summary-item {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8px;
|
|
|
- font-size: 13px;
|
|
|
- padding: 8px 12px;
|
|
|
- border-radius: 10px;
|
|
|
- background: rgba(255, 255, 255, 0.03);
|
|
|
- border: 1px solid rgba(255, 255, 255, 0.04);
|
|
|
- transition: all 0.3s;
|
|
|
+.chart-card .chart {
|
|
|
+ height: 260px;
|
|
|
}
|
|
|
|
|
|
-.hazard-summary-item:hover {
|
|
|
- background: rgba(255, 255, 255, 0.06);
|
|
|
- border-color: rgba(255, 255, 255, 0.08);
|
|
|
+.chart-card :deep(.el-card__header) {
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-bottom: 1px solid #e1ebf3;
|
|
|
+ background: #fbfdff;
|
|
|
}
|
|
|
|
|
|
-.hazard-dot {
|
|
|
- width: 8px;
|
|
|
- height: 24px;
|
|
|
- display: inline-block;
|
|
|
- border-radius: 4px;
|
|
|
- box-shadow: 0 0 10px currentColor;
|
|
|
-}
|
|
|
-
|
|
|
-.hazard-chart {
|
|
|
- height: 160px;
|
|
|
+.chart-card :deep(.el-card__body) {
|
|
|
+ padding: 10px 12px;
|
|
|
}
|
|
|
|
|
|
-.equipment-wrap {
|
|
|
- height: calc(100% - 38px);
|
|
|
-}
|
|
|
+@media (max-width: 1200px) {
|
|
|
+ .metric-grid {
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
+ }
|
|
|
|
|
|
-:deep(.BMap_cpyCtrl),
|
|
|
-:deep(.anchorBL) {
|
|
|
- display: none !important;
|
|
|
-}
|
|
|
+ .workbench-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
|
|
|
-.deerflow-mark {
|
|
|
- position: fixed;
|
|
|
- bottom: 14px;
|
|
|
- right: 18px;
|
|
|
- z-index: 10;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 6px;
|
|
|
- text-decoration: none;
|
|
|
- opacity: 0.28;
|
|
|
- transition: opacity 0.4s;
|
|
|
-}
|
|
|
+ .chart-grid {
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ }
|
|
|
|
|
|
-.deerflow-mark:hover {
|
|
|
- opacity: 0.6;
|
|
|
+ .trend-card {
|
|
|
+ grid-column: 1 / -1;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-.deerflow-diamond {
|
|
|
- font-size: 10px;
|
|
|
- color: #8fe5ff;
|
|
|
-}
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .page-heading,
|
|
|
+ .heading-actions {
|
|
|
+ align-items: stretch;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
|
|
|
-.deerflow-text {
|
|
|
- font-size: 11px;
|
|
|
- letter-spacing: 1px;
|
|
|
- color: #8fe5ff;
|
|
|
- font-weight: 500;
|
|
|
-}
|
|
|
+ .metric-grid,
|
|
|
+ .chart-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
|
|
|
-@media (max-width: 1500px) {
|
|
|
- .screen-body {
|
|
|
- grid-template-columns: 320px 1fr 320px;
|
|
|
+ .trend-card {
|
|
|
+ grid-column: auto;
|
|
|
}
|
|
|
|
|
|
- .header-title {
|
|
|
- font-size: 24px;
|
|
|
+ .map-legend {
|
|
|
+ display: none;
|
|
|
}
|
|
|
|
|
|
- .summary-item,
|
|
|
- .map-kpi {
|
|
|
- min-width: 96px;
|
|
|
+ .quick-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
}
|
|
|
}
|
|
|
</style>
|