Files
monitor/src/App.vue
2026-01-15 19:27:23 +08:00

1425 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div
class="monitor-container"
:class="{ 'is-top2': currentStyle === 'top2', 'is-top3': currentStyle === 'top3' }"
>
<!-- 样式切换下拉框 -->
<div
class="style-switcher"
:class="{ 'is-top2': currentStyle === 'top2', 'is-top3': currentStyle === 'top3' }"
>
<label class="style-label" for="style-select">原型迭代</label>
<select id="style-select" v-model="currentStyle" class="style-select">
<option value="top3">原型-3</option>
<option value="top2">原型-2</option>
<option value="circle">原型-1</option>
</select>
</div>
<template v-if="currentStyle === 'top2' || currentStyle === 'top3'">
<div
class="top2-stage"
:class="topLayoutClass"
ref="top2Stage"
:style="{ '--top2-scale': top2Scale }"
>
<div class="top2-scale">
<div class="top2-layout">
<div class="top2-header" data-node-id="153:2111" :style="headerStyle">
<div class="top2-title" data-node-id="153:2112">火情综合监控看板</div>
</div>
<div class="top2-content">
<div class="top2-left-col">
<div class="panel-outline panel-left-top" data-node-id="153:2087">
<div class="panel-label label-metric-11" data-node-id="153:2098">视联拉流质量统计</div>
</div>
<div v-if="currentStyle === 'top3'" class="panel-outline panel-left-middle" data-node-id="153:2089">
<div class="panel-label label-metric-21" data-node-id="153:2099">AI分析质量监控</div>
</div>
<div class="panel-outline panel-left-bottom" data-node-id="271:27">
<div
class="panel-label"
:class="currentStyle === 'top3' ? 'label-metric-31' : 'label-metric-21'"
data-node-id="271:28"
>
{{ currentStyle === 'top3' ? '网络质量分析' : 'AI分析质量监控' }}
</div>
</div>
</div>
<div class="top2-center-col">
<div v-if="currentStyle === 'top3'" class="top2-cards" data-node-id="270:19">
<div class="top2-card" data-node-id="277:79">
<div class="top2-card-icon">
<img :src="top2CardIcons.device" alt="" />
</div>
<div class="top2-card-text">
<div class="top2-card-label">火情设备数</div>
<div class="top2-card-value">54781</div>
<div class="top2-card-sub">截止今日数量</div>
</div>
</div>
<div class="top2-card" data-node-id="279:46">
<div class="top2-card-icon">
<img :src="top2CardIcons.stream" alt="" />
</div>
<div class="top2-card-text">
<div class="top2-card-label">拉流质量</div>
<div class="top2-card-value">85.2%</div>
<div class="top2-card-sub">拉流成功率指标</div>
</div>
</div>
<div class="top2-card" data-node-id="279:54">
<div class="top2-card-icon">
<img :src="top2CardIcons.ai" alt="" />
</div>
<div class="top2-card-text">
<div class="top2-card-label">AI分析质量</div>
<div class="top2-card-value">99.5%</div>
<div class="top2-card-sub">AI请求成功率</div>
</div>
</div>
<div class="top2-card" data-node-id="279:62">
<div class="top2-card-icon">
<img :src="top2CardIcons.cut" alt="" />
</div>
<div class="top2-card-text">
<div class="top2-card-label">切图质量</div>
<div class="top2-card-value">99.5%</div>
<div class="top2-card-sub">抽帧切图成功率</div>
</div>
</div>
</div>
<div v-if="currentStyle === 'top3'" class="top3-center-block" data-node-id="303:722">
<div class="top2-center" data-node-id="303:723">
<div class="svg-wrapper is-top3" ref="svgWrapper" v-html="svgContent"></div>
</div>
<img class="top3-map-frame" :src="mapKuang" alt="" />
</div>
<div v-else class="top2-center" data-node-id="162:5">
<div class="svg-wrapper is-top2" ref="svgWrapper" v-html="svgContent"></div>
</div>
<div class="top2-bottom-row">
<div class="panel-outline panel-center-bottom-left" data-node-id="153:2091">
<div class="panel-label label-metric-22" data-node-id="153:2100">
{{ currentStyle === 'top3' ? '切图指标' : '监控指标区22' }}
</div>
</div>
<div class="panel-outline panel-center-bottom-right" data-node-id="153:2092">
<div class="panel-label label-metric-23" data-node-id="153:2102">
{{ currentStyle === 'top3' ? '魔方指标' : '监控指标区23' }}
</div>
</div>
</div>
</div>
<div class="top2-right-col">
<template v-if="currentStyle === 'top3'">
<div class="panel-outline top3-board top3-alarm" data-node-id="328:1647">
<div class="top3-board-title">告警看板</div>
<div class="top3-board-table">
<div class="top3-board-col top3-rank-col">
<div class="top3-table-head"></div>
<div class="top3-table-cell top3-rank-cell"><span class="rank-badge rank-1">1</span></div>
<div class="top3-table-cell top3-rank-cell"><span class="rank-badge rank-2">3</span></div>
<div class="top3-table-cell top3-rank-cell"><span class="rank-badge rank-3">3</span></div>
</div>
<div class="top3-board-col top3-info-col">
<div class="top3-table-head top3-table-head-left">告警信息</div>
<div class="top3-table-cell top3-info-cell">告警1</div>
<div class="top3-table-cell top3-info-cell">告警2</div>
<div class="top3-table-cell top3-info-cell">告警3</div>
</div>
<div class="top3-board-col top3-metric-col">
<div class="top3-table-head top3-table-head-right">指标</div>
<div class="top3-table-cell top3-metric-cell">网络</div>
<div class="top3-table-cell top3-metric-cell">视联网</div>
<div class="top3-table-cell top3-metric-cell">AI分析</div>
</div>
</div>
<div class="top3-board-bottom">
<div class="top3-bottom-title">告警内容</div>
<div class="top3-bottom-line"></div>
<div class="top3-bottom-line"></div>
<div class="top3-bottom-line"></div>
</div>
</div>
<div class="panel-outline top3-board top3-fault" data-node-id="328:1692">
<div class="top3-board-title">故障看板</div>
<div class="top3-board-table">
<div class="top3-board-col top3-rank-col">
<div class="top3-table-head"></div>
<div class="top3-table-cell top3-rank-cell"><span class="rank-badge rank-1">1</span></div>
<div class="top3-table-cell top3-rank-cell"><span class="rank-badge rank-2">3</span></div>
<div class="top3-table-cell top3-rank-cell"><span class="rank-badge rank-3">3</span></div>
</div>
<div class="top3-board-col top3-info-col">
<div class="top3-table-head top3-table-head-left">故障记录</div>
<div class="top3-table-cell top3-info-cell">故障1</div>
<div class="top3-table-cell top3-info-cell">故障2</div>
<div class="top3-table-cell top3-info-cell">故障3</div>
</div>
<div class="top3-board-col top3-metric-col">
<div class="top3-table-head top3-table-head-right">指标</div>
<div class="top3-table-cell top3-metric-cell">网络</div>
<div class="top3-table-cell top3-metric-cell">视联网拉流</div>
<div class="top3-table-cell top3-metric-cell">AI分析</div>
</div>
</div>
<div class="top3-board-bottom">
<div class="top3-bottom-title">故障根因</div>
<div class="top3-bottom-line"></div>
<div class="top3-bottom-line"></div>
<div class="top3-bottom-line"></div>
</div>
</div>
</template>
<template v-else>
<div class="panel-outline panel-right-top" data-node-id="153:2088">
<div class="panel-label label-alarm" data-node-id="153:2095">告警看板</div>
</div>
<div class="panel-outline panel-right-bottom" data-node-id="153:2090">
<div class="panel-label label-fault" data-node-id="153:2096">故障看板</div>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
<template v-else>
<h1 class="title">火情监控全链路业务监控视图</h1>
<div class="svg-wrapper" ref="svgWrapper" v-html="svgContent"></div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, nextTick, watch, computed } from 'vue'
import videoIcon from './assets/video.svg'
import playIcon from './assets/play.svg'
import aiIcon from './assets/ai.svg'
import picIcon from './assets/pic.svg'
import mapKuang from './assets/map-kuang.svg'
import headerBack from './assets/header-back.svg'
type StyleType = 'circle' | 'top2' | 'top3'
const svgWrapper = ref<HTMLElement | null>(null)
const svgContent = ref('')
const currentStyle = ref<StyleType>('top3')
const top2Stage = ref<HTMLElement | null>(null)
const top2Scale = ref(1)
const TOP_LAYOUTS = {
top2: { width: 1296, height: 692 },
top3: { width: 1438, height: 929 },
}
let top2ResizeObserver: ResizeObserver | null = null
const top2CardIcons = {
device: videoIcon,
stream: playIcon,
ai: aiIcon,
cut: picIcon,
}
const updateTop2Scale = () => {
const stage = top2Stage.value
if (!stage) return
if (currentStyle.value === 'circle') return
const layout = TOP_LAYOUTS[currentStyle.value]
if (!layout) return
const { width, height } = stage.getBoundingClientRect()
if (!width || !height) return
const scaleX = width / layout.width
const scaleY = height / layout.height
top2Scale.value = Math.min(scaleX, scaleY)
}
const attachTop2Observer = () => {
const stage = top2Stage.value
if (!stage) return
if (!top2ResizeObserver) {
top2ResizeObserver = new ResizeObserver(() => {
updateTop2Scale()
})
} else {
top2ResizeObserver.disconnect()
}
top2ResizeObserver.observe(stage)
}
const detachTop2Observer = () => {
if (!top2ResizeObserver) return
top2ResizeObserver.disconnect()
}
const loadSvg = async (style: StyleType) => {
try {
const fileName = style === 'circle' ? 'monitor.svg' : 'top2.svg'
const response = await fetch(`/${fileName}`)
const content = await response.text()
svgContent.value = content
// 等待 DOM 更新后添加动画
setTimeout(() => {
if (style === 'circle') {
addFlowingAnimation()
} else {
addTop2Animation()
}
}, 100)
} catch (error) {
console.error('加载 SVG 失败:', error)
}
}
onMounted(() => {
nextTick(() => {
loadSvg(currentStyle.value)
if (currentStyle.value === 'top2' || currentStyle.value === 'top3') {
attachTop2Observer()
updateTop2Scale()
}
})
})
onBeforeUnmount(() => {
detachTop2Observer()
})
watch(currentStyle, async (style) => {
await nextTick()
loadSvg(style)
if (style === 'top2' || style === 'top3') {
attachTop2Observer()
updateTop2Scale()
} else {
detachTop2Observer()
}
})
const topLayoutClass = computed(() => (
currentStyle.value === 'top3' ? 'layout-top3' : 'layout-top2'
))
const headerStyle = computed(() => (
currentStyle.value === 'top3' ? { '--header-back': `url(${headerBack})` } : {}
))
const addFlowingAnimation = () => {
if (!svgWrapper.value) return
const svg = svgWrapper.value.querySelector('svg')
if (!svg) {
console.log('SVG not found')
return
}
svg.querySelectorAll('.flowing-line').forEach((node) => node.remove())
// 查找所有可能的连线元素
const allPaths = svg.querySelectorAll('path, line, polyline')
console.log('Found elements:', allPaths.length)
let animatedCount = 0
allPaths.forEach((element, index) => {
const el = element as SVGPathElement | SVGLineElement | SVGPolylineElement
// 检查是否有描边
const hasStroke = el.getAttribute('stroke') && el.getAttribute('stroke') !== 'none'
const hasFill = el.getAttribute('fill') && el.getAttribute('fill') !== 'none'
// 只处理有描边且填充为none或没有填充的元素这些是连线
if (!hasStroke || (hasFill && el.getAttribute('fill') !== 'transparent')) {
return
}
// 跳过顶部第一条虚线shape582
const elementId = el.getAttribute('id')
if (elementId === 'shape582') {
return
}
const strokeWidth = parseFloat(el.getAttribute('stroke-width') || '1')
if (strokeWidth < 0.5) return
animatedCount++
// 保存原始描边颜色和宽度
const originalStroke = el.getAttribute('stroke') || '#595959'
const originalWidth = strokeWidth
// 判断是否为红色线条
const isRed = originalStroke.toLowerCase().includes('red') ||
originalStroke.toLowerCase().includes('#eb') ||
originalStroke.toLowerCase().includes('#ff') ||
originalStroke.toLowerCase().includes('#f') ||
originalStroke.toLowerCase().includes('rgb(235') ||
originalStroke.toLowerCase().includes('rgb(255')
// 根据线条颜色选择流动颜色
const flowColor = isRed ? '#EB5017' : '#00ff88'
// 创建流动层(克隆原线条)
const flowLine = el.cloneNode(true) as SVGElement
flowLine.setAttribute('stroke', flowColor)
flowLine.setAttribute('stroke-width', String(originalWidth * 1.2))
flowLine.setAttribute('stroke-linecap', 'round')
flowLine.setAttribute('stroke-opacity', '0.7')
flowLine.setAttribute('fill', 'none')
flowLine.classList.add('flowing-line')
// 计算路径长度
let pathLength = 0
if (flowLine instanceof SVGPathElement) {
pathLength = flowLine.getTotalLength()
} else if (flowLine instanceof SVGLineElement) {
const x1 = parseFloat(flowLine.getAttribute('x1') || '0')
const y1 = parseFloat(flowLine.getAttribute('y1') || '0')
const x2 = parseFloat(flowLine.getAttribute('x2') || '0')
const y2 = parseFloat(flowLine.getAttribute('y2') || '0')
pathLength = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
}
// 设置虚线动画 - 更明显的流动效果
const dashLength = 40
const gapLength = 80
flowLine.style.strokeDasharray = `${dashLength} ${gapLength}`
flowLine.style.strokeDashoffset = String(pathLength)
// 插入流动层到原线条之后
el.parentNode?.insertBefore(flowLine, el.nextSibling)
// 添加 CSS 动画 - 减慢速度到 8-12 秒
const duration = 8 + (index % 5)
flowLine.style.animation = `flowLine ${duration}s linear infinite`
flowLine.style.animationDelay = `${(index * 0.5) % 3}s`
})
console.log('Animated lines:', animatedCount)
// 添加 CSS 动画样式
if (!document.getElementById('flowing-animation-style')) {
const style = document.createElement('style')
style.id = 'flowing-animation-style'
style.textContent = `
@keyframes flowLine {
from {
stroke-dashoffset: 1000;
}
to {
stroke-dashoffset: -1000;
}
}
.flowing-line {
filter: drop-shadow(0 0 6px currentColor);
pointer-events: none;
}
`
document.head.appendChild(style)
}
}
// Top2 样式的动画效果
const addTop2Animation = () => {
if (!svgWrapper.value) return
const svg = svgWrapper.value.querySelector('svg')
if (!svg) {
console.log('SVG not found')
return
}
svg.querySelectorAll('.flowing-line-top2').forEach((node) => node.remove())
// 查找所有可能的连线元素
const allPaths = svg.querySelectorAll('path, line, polyline')
console.log('Found elements for top2:', allPaths.length)
let animatedCount = 0
allPaths.forEach((element, index) => {
const el = element as SVGPathElement | SVGLineElement | SVGPolylineElement
// 检查是否有描边
const hasStroke = el.getAttribute('stroke') && el.getAttribute('stroke') !== 'none'
const hasFill = el.getAttribute('fill') && el.getAttribute('fill') !== 'none'
// 只处理有描边且填充为none或没有填充的元素这些是连线
if (!hasStroke || (hasFill && el.getAttribute('fill') !== 'transparent')) {
return
}
const strokeWidth = parseFloat(el.getAttribute('stroke-width') || '1')
if (strokeWidth < 0.5) return
animatedCount++
// 保存原始描边颜色和宽度
const originalStroke = el.getAttribute('stroke') || '#595959'
const originalWidth = strokeWidth
// 判断是否为特殊颜色线条
const isBlue = originalStroke.toLowerCase().includes('blue') ||
originalStroke.toLowerCase().includes('#878fd3') ||
originalStroke.toLowerCase().includes('#3469f1')
const isRed = originalStroke.toLowerCase().includes('red') ||
originalStroke.toLowerCase().includes('#eb') ||
originalStroke.toLowerCase().includes('#cf0e30')
// 使用更鲜艳的科技感配色方案
let flowColor = '#00F5FF' // 亮青色(强科技感)
let glowColor = '#00F5FF'
let shadowColor = 'rgba(0, 245, 255, 0.6)'
if (isBlue) {
flowColor = '#00D4FF' // 明亮蓝
glowColor = '#4D9FFF'
shadowColor = 'rgba(0, 212, 255, 0.6)'
}
if (isRed) {
flowColor = '#FF1744' // 鲜艳红(警告色)
glowColor = '#FF4081'
shadowColor = 'rgba(255, 23, 68, 0.6)'
}
// 创建流动层(克隆原线条)
const flowLine = el.cloneNode(true) as SVGElement
flowLine.setAttribute('stroke', flowColor)
flowLine.setAttribute('stroke-width', String(originalWidth * 2.5))
flowLine.setAttribute('stroke-linecap', 'round')
flowLine.setAttribute('stroke-opacity', '1')
flowLine.setAttribute('fill', 'none')
flowLine.classList.add('flowing-line-top2')
flowLine.style.setProperty('--glow-color', glowColor)
flowLine.style.setProperty('--shadow-color', shadowColor)
// 计算路径长度
let pathLength = 0
if (flowLine instanceof SVGPathElement) {
pathLength = flowLine.getTotalLength()
} else if (flowLine instanceof SVGLineElement) {
const x1 = parseFloat(flowLine.getAttribute('x1') || '0')
const y1 = parseFloat(flowLine.getAttribute('y1') || '0')
const x2 = parseFloat(flowLine.getAttribute('x2') || '0')
const y2 = parseFloat(flowLine.getAttribute('y2') || '0')
pathLength = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
}
// 设置虚线动画 - 更明显的流动效果
const dashLength = 60
const gapLength = 120
flowLine.style.strokeDasharray = `${dashLength} ${gapLength}`
flowLine.style.strokeDashoffset = String(pathLength)
// 插入流动层到原线条之后
el.parentNode?.insertBefore(flowLine, el.nextSibling)
// 添加 CSS 动画 - 流畅快速的速度
const duration = 4 + (index % 3)
const pulseSpeed = duration * 0.4
flowLine.style.animation = `flowLineTop2 ${duration}s linear infinite, pulseGlow ${pulseSpeed}s ease-in-out infinite`
flowLine.style.animationDelay = `${(index * 0.3) % 2}s`
})
console.log('Animated top2 lines:', animatedCount)
// 添加 Top2 CSS 动画样式
if (!document.getElementById('flowing-animation-top2-style')) {
const style = document.createElement('style')
style.id = 'flowing-animation-top2-style'
style.textContent = `
@keyframes flowLineTop2 {
0% {
stroke-dashoffset: 1000;
}
100% {
stroke-dashoffset: -1000;
}
}
@keyframes pulseGlow {
0%, 100% {
filter: drop-shadow(0 0 6px var(--glow-color))
drop-shadow(0 0 12px var(--glow-color))
drop-shadow(0 0 18px var(--shadow-color));
stroke-opacity: 1;
}
50% {
filter: drop-shadow(0 0 12px var(--glow-color))
drop-shadow(0 0 24px var(--glow-color))
drop-shadow(0 0 36px var(--shadow-color))
drop-shadow(0 0 48px var(--shadow-color));
stroke-opacity: 1;
}
}
.flowing-line-top2 {
pointer-events: none;
mix-blend-mode: screen;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.flowing-line-top2:hover {
filter: drop-shadow(0 0 20px var(--glow-color))
drop-shadow(0 0 40px var(--shadow-color)) !important;
}
`
document.head.appendChild(style)
}
}
</script>
<style scoped>
.monitor-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 10px;
position: relative;
}
.style-switcher {
display: flex;
align-items: center;
gap: 12px;
margin-top: 10px;
}
.style-switcher.is-top2,
.style-switcher.is-top3 {
margin-top: 0;
position: absolute;
top: 24px;
right: 24px;
z-index: 5;
}
.style-label {
font-size: 14px;
font-weight: 600;
color: rgba(255, 255, 255, 0.75);
}
.style-select {
padding: 8px 14px;
font-size: 14px;
font-weight: 600;
color: #ffffff;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 10px;
cursor: pointer;
backdrop-filter: blur(10px);
outline: none;
}
.style-select option {
color: #0e1a3f;
}
.title {
font-size: 2.5rem;
font-weight: 700;
color: white;
text-align: center;
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
letter-spacing: 2px;
padding: 20px 30px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
border: 2px solid rgba(255, 255, 255, 0.2);
animation: titleGlow 3s ease-in-out infinite;
width: 100%;
max-width: 1400px;
box-sizing: border-box;
}
@keyframes titleGlow {
0%, 100% {
box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
}
50% {
box-shadow: 0 0 40px rgba(255, 255, 255, 0.5);
}
}
.svg-wrapper {
flex: 1;
width: 100%;
max-width: 1400px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: auto;
min-height: 0;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
}
.monitor-container.is-top2,
.monitor-container.is-top3 {
padding: 0;
gap: 0;
align-items: stretch;
justify-content: stretch;
background: #182b69;
}
.top2-stage {
flex: 1;
width: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: #182b69;
}
.top2-scale {
position: relative;
flex-shrink: 0;
}
.top2-layout {
position: absolute;
left: 0;
top: 0;
background: #182b69;
border-radius: 0;
overflow: hidden;
transform: scale(var(--top2-scale));
transform-origin: top left;
}
.layout-top2 .top2-scale {
width: calc(1296px * var(--top2-scale));
height: calc(692px * var(--top2-scale));
}
.layout-top3 .top2-scale {
width: calc(1438px * var(--top2-scale));
height: calc(929px * var(--top2-scale));
}
.layout-top2 .top2-layout {
width: 1296px;
height: 692px;
}
.layout-top3 .top2-layout {
width: 1438px;
height: 929px;
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
box-sizing: border-box;
}
.top2-header {
box-sizing: border-box;
}
.layout-top2 .top2-header {
position: absolute;
left: 16px;
top: 16px;
width: 1264px;
height: 50px;
border-radius: 16px;
background: #e5e5e5;
border: 0.297px solid #9fbaff;
}
.layout-top3 .top2-header {
height: 72px;
border-radius: 0;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
background-image: var(--header-back);
background-repeat: no-repeat;
background-position: center;
background-size: 1120px 72px;
}
.top2-title {
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-weight: 700;
}
.layout-top2 .top2-title {
position: absolute;
left: 516.1px;
top: 7.78px;
font-size: 28.491px;
color: #182b69;
letter-spacing: 0.3px;
}
.layout-top3 .top2-title {
font-size: 32px;
color: #f9d1b0;
letter-spacing: 0.5px;
text-align: center;
}
.layout-top2 .top2-content {
position: absolute;
left: 16px;
top: 82px;
width: 1264px;
height: 594px;
}
.layout-top3 .top2-content {
width: 100%;
height: 809px;
display: flex;
gap: 16px;
}
.layout-top2 .top2-left-col {
position: absolute;
left: 0;
top: 26.055px;
width: 232.377px;
height: 541.889px;
}
.layout-top3 .top2-left-col {
width: 315px;
height: 809px;
display: flex;
flex-direction: column;
gap: 16px;
}
.layout-top2 .top2-center-col {
position: absolute;
left: 248.377px;
top: 25.15px;
width: 744.126px;
height: 543.699px;
padding: 0;
}
.layout-top3 .top2-center-col {
width: 744px;
height: 809px;
display: flex;
flex-direction: column;
gap: 8px;
padding: 0;
}
.layout-top2 .top2-right-col {
position: absolute;
left: 1008.503px;
top: 25.907px;
width: 232.377px;
height: 542.186px;
}
.layout-top3 .top2-right-col {
width: 315px;
height: 809px;
display: flex;
flex-direction: column;
gap: 16px;
}
.top3-board {
width: 315px;
height: 396.5px;
border-radius: 16px;
display: flex;
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.top3-board-title {
font-family: 'Inter', 'Noto Sans JP', sans-serif;
font-size: 24px;
font-weight: 700;
color: #f02525;
text-align: center;
line-height: 40px;
}
.top3-board-table {
width: 100%;
height: 156px;
display: flex;
}
.top3-board-col {
display: flex;
flex-direction: column;
align-items: stretch;
}
.top3-rank-col {
width: 36px;
}
.top3-info-col {
width: 146px;
}
.top3-metric-col {
width: 133px;
}
.top3-table-head {
height: 40px;
background: #043272;
display: flex;
align-items: center;
justify-content: center;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 16px;
color: rgba(255, 255, 255, 0.75);
}
.top3-table-head-left {
justify-content: flex-start;
padding: 0 10px;
}
.top3-table-head-right {
justify-content: flex-start;
padding: 0 0 0 16px;
text-align: left;
}
.top3-table-cell {
display: flex;
align-items: center;
flex: 1;
}
.top3-rank-cell {
justify-content: center;
}
.top3-info-cell {
padding: 0 10px;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 18px;
color: #ffffff;
line-height: 28px;
}
.top3-metric-cell {
justify-content: flex-start;
padding: 0 0 0 16px;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 20px;
color: #4de4ff;
}
.rank-badge {
width: 24px;
height: 24px;
border-radius: 2px;
display: inline-flex;
align-items: center;
justify-content: center;
font-family: 'Mark Pro', 'Inter', sans-serif;
font-size: 16px;
font-weight: 700;
color: #ffffff;
letter-spacing: 0.75px;
}
.rank-1 {
background: #c85b3f;
}
.rank-2 {
background: #b99a3a;
}
.rank-3 {
background: #1fb2a4;
}
.top3-board-bottom {
width: 100%;
height: 180.5px;
background: #ffffff;
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
position: relative;
box-sizing: border-box;
}
.top3-bottom-title {
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 16px;
font-weight: 600;
color: rgba(0, 0, 0, 0.6);
position: absolute;
left: 13.03px;
top: 15.427px;
}
.top3-bottom-line {
width: 246.568px;
height: 1.556px;
background: rgba(240, 226, 226, 0.9);
position: absolute;
left: 34.759px;
}
.top3-board-bottom .top3-bottom-line:nth-of-type(2) {
top: 72.508px;
left: 35.845px;
}
.top3-board-bottom .top3-bottom-line:nth-of-type(3) {
top: 109.548px;
}
.top3-board-bottom .top3-bottom-line:nth-of-type(4) {
top: 146.573px;
}
.panel-outline {
border: 0.297px solid #f0e2e2;
border-radius: 10.684px;
background: rgba(217, 217, 217, 0);
box-sizing: border-box;
}
.layout-top2 .panel-outline {
position: absolute;
}
.layout-top3 .panel-outline {
position: relative;
}
.top3-board.panel-outline {
border-radius: 16px;
}
.layout-top2 .panel-left-top {
left: 0;
top: 0;
width: 232.377px;
height: 259.977px;
}
.layout-top3 .panel-left-top {
width: 315px;
height: 259px;
}
.layout-top3 .panel-left-middle {
width: 315px;
height: 259px;
}
.layout-top2 .panel-left-bottom {
left: 0;
top: 275.977px;
width: 232.377px;
height: 265.912px;
}
.layout-top3 .panel-left-bottom {
width: 315px;
height: 259px;
}
.layout-top2 .panel-right-top {
left: 0;
top: 0;
width: 232.377px;
height: 259.977px;
}
.layout-top3 .panel-right-top {
width: 315px;
height: 359px;
}
.layout-top2 .panel-right-bottom {
left: 0;
top: 275.977px;
width: 232.377px;
height: 266.209px;
}
.layout-top3 .panel-right-bottom {
width: 315px;
height: 359px;
}
.layout-top2 .panel-center-bottom-left {
left: 0;
top: 0;
width: 360.668px;
height: 175.692px;
}
.layout-top2 .panel-center-bottom-right {
left: 386.668px;
top: 0;
width: 357.216px;
height: 175.692px;
}
.layout-top3 .panel-center-bottom-left {
width: auto;
flex: 1;
height: 242.813px;
}
.layout-top3 .panel-center-bottom-right {
width: auto;
flex: 1;
height: 242.813px;
}
.layout-top2 .top2-center {
position: absolute;
left: 0;
top: 0;
width: 743.928px;
height: 352.007px;
}
.layout-top3 .top2-center {
width: 696.481px;
height: 333.004px;
position: relative;
overflow: visible;
}
.layout-top2 .top2-bottom-row {
position: absolute;
left: 0;
top: 368.007px;
width: 744.126px;
height: 175.692px;
}
.layout-top3 .top2-bottom-row {
width: 744px;
height: 242.818px;
display: flex;
gap: 16px;
align-items: stretch;
}
.top2-cards {
height: 114px;
display: flex;
gap: 8px;
align-items: center;
padding: 8px 0;
box-sizing: border-box;
}
.top2-card {
width: 180.032px;
height: 98px;
border-radius: 16px;
background: rgba(37, 67, 165, 0.51);
display: flex;
gap: 12px;
align-items: center;
padding: 8px;
box-sizing: border-box;
}
.top2-card-icon {
width: 51px;
height: 49px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
}
.top2-card-icon::before {
content: '';
position: absolute;
width: 49px;
height: 49px;
border-radius: 50%;
border: 1.5px solid rgba(255, 255, 255, 0.7);
background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.35), rgba(37, 67, 165, 0.15) 60%, rgba(37, 67, 165, 0) 100%);
box-shadow: 0 0 10px rgba(0, 245, 255, 0.35);
}
.top2-card-icon img {
width: 28px;
height: 28px;
display: block;
position: relative;
z-index: 1;
}
.top2-card-text {
display: flex;
flex-direction: column;
gap: 4px;
}
.top2-card-label {
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 18px;
font-weight: 600;
color: #ffffff;
}
.top2-card-value {
font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 24px;
font-weight: 700;
color: #ffffff;
line-height: 1;
}
.top2-card-sub {
font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 12px;
font-weight: 700;
color: rgba(191, 191, 191, 0.61);
}
.layout-top2 .top2-cards,
.layout-top2 .top2-subheader {
display: none;
}
.layout-top3 .top2-cards {
width: 744px;
}
.top2-subheader {
height: 51px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 28px;
font-weight: 700;
color: #ffffff;
}
.layout-top3 .top2-subheader {
display: none;
}
.top3-center-block {
width: 744px;
height: 436.182px;
position: relative;
}
.top3-center-block .top2-center {
position: absolute;
left: 24.982px;
top: 65px;
width: 696.481px;
height: 333.004px;
z-index: 1;
}
.top3-center-block .svg-wrapper {
position: relative;
z-index: 1;
}
.top3-map-frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: fill;
pointer-events: none;
z-index: 2;
}
.svg-wrapper.is-top2,
.svg-wrapper.is-top3 {
flex: none;
width: 100%;
height: 100%;
max-width: none;
padding: 0;
background: transparent;
border-radius: 0;
box-shadow: none;
overflow: hidden;
}
.layout-top2 .panel-label {
position: absolute;
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 18.994px;
font-weight: 700;
color: #ffffff;
letter-spacing: 0.2px;
}
.layout-top3 .panel-label {
position: absolute;
left: 50%;
top: 20px;
transform: translate(-50%, -50%);
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 24px;
font-weight: 700;
color: #ffffff;
letter-spacing: 0.2px;
text-align: center;
width: 220px;
}
.layout-top2 .label-metric-11 {
left: 61.73px;
top: 91.7px;
}
.layout-top2 .label-metric-21 {
left: 61.73px;
top: 121.68px;
}
.layout-top2 .label-metric-22 {
left: 132.11px;
top: 76.27px;
}
.layout-top2 .label-metric-23 {
left: 126.11px;
top: 76.27px;
}
.layout-top2 .label-alarm {
left: 78.35px;
top: 18.7px;
color: #f02525;
}
.layout-top2 .label-fault {
left: 78.35px;
top: 22.85px;
color: #ffffff;
}
.layout-top3 .label-alarm {
color: #f02525;
}
.svg-wrapper :deep(svg) {
width: 100%;
height: auto;
max-height: 100%;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.1));
touch-action: pan-x pan-y;
}
.svg-wrapper.is-top2 :deep(svg),
.svg-wrapper.is-top3 :deep(svg) {
height: 100%;
max-height: none;
filter: none;
}
.svg-wrapper.is-top2 :deep(#shape1),
.svg-wrapper.is-top3 :deep(#shape1) {
fill: transparent;
stroke: none;
}
:deep(.flowing-line) {
transition: all 0.3s ease;
}
:deep(.flowing-line:hover) {
stroke-width: 6 !important;
filter: drop-shadow(0 0 8px #00ff88);
}
/* 平板适配 */
@media (max-width: 1024px) {
.title {
font-size: 2rem;
padding: 15px 20px;
letter-spacing: 1px;
}
.monitor-container:not(.is-top2):not(.is-top3) .svg-wrapper {
padding: 20px;
}
}
/* 手机适配 */
@media (max-width: 768px) {
.monitor-container:not(.is-top2):not(.is-top3) {
gap: 15px;
padding: 10px;
}
.title {
font-size: 1.5rem;
padding: 12px 16px;
letter-spacing: 0.5px;
border-radius: 15px;
}
.monitor-container:not(.is-top2):not(.is-top3) .svg-wrapper {
padding: 15px;
border-radius: 15px;
}
}
/* 小屏手机适配 */
@media (max-width: 480px) {
.monitor-container:not(.is-top2):not(.is-top3) {
gap: 10px;
padding: 8px;
}
.title {
font-size: 1.2rem;
padding: 10px 12px;
letter-spacing: 0.3px;
border-radius: 12px;
}
.monitor-container:not(.is-top2):not(.is-top3) .svg-wrapper {
padding: 10px;
border-radius: 12px;
}
}
</style>