Files
monitor/src/App.vue
2026-01-14 13:15:32 +08:00

527 lines
14 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' }">
<h1 class="title">火情监控全链路业务监控视图</h1>
<!-- 样式切换按钮 -->
<div class="style-switcher">
<button
:class="['style-btn', { active: currentStyle === 'circle' }]"
@click="switchStyle('circle')"
>
圆形样式
</button>
<button
:class="['style-btn', { active: currentStyle === 'top2' }]"
@click="switchStyle('top2')"
>
Top2样式
</button>
</div>
<div
class="svg-wrapper"
:class="{ 'is-top2': currentStyle === 'top2' }"
ref="svgWrapper"
v-html="svgContent"
></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
type StyleType = 'circle' | 'top2'
const svgWrapper = ref<HTMLElement | null>(null)
const svgContent = ref('')
const currentStyle = ref<StyleType>('circle')
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)
}
}
const switchStyle = (style: StyleType) => {
currentStyle.value = style
loadSvg(style)
}
onMounted(() => {
loadSvg('circle')
})
const addFlowingAnimation = () => {
if (!svgWrapper.value) return
const svg = svgWrapper.value.querySelector('svg')
if (!svg) {
console.log('SVG not found')
return
}
// 查找所有可能的连线元素
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
}
// 查找所有可能的连线元素
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;
}
.style-switcher {
display: flex;
gap: 10px;
margin-top: 10px;
}
.style-btn {
padding: 10px 24px;
font-size: 16px;
font-weight: 600;
color: rgba(255, 255, 255, 0.7);
background: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.style-btn:hover {
color: rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 255, 255, 0.2);
}
.style-btn.active {
color: white;
background: linear-gradient(135deg, rgba(0, 245, 255, 0.25), rgba(77, 159, 255, 0.25));
border-color: #00F5FF;
box-shadow: 0 0 20px rgba(0, 245, 255, 0.4),
0 0 40px rgba(0, 245, 255, 0.2),
inset 0 0 20px rgba(0, 245, 255, 0.1);
}
.style-btn.active:hover {
background: linear-gradient(135deg, rgba(0, 245, 255, 0.35), rgba(77, 159, 255, 0.35));
box-shadow: 0 0 25px rgba(0, 245, 255, 0.5),
0 0 50px rgba(0, 245, 255, 0.3),
inset 0 0 25px rgba(0, 245, 255, 0.15);
}
.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 {
padding: 8px;
gap: 12px;
}
.svg-wrapper.is-top2 {
max-width: none;
width: 100%;
height: 100%;
padding: 0;
background: transparent;
border-radius: 55px;
box-shadow: none;
overflow: hidden;
}
.svg-wrapper.is-top2 :deep(svg) {
width: 100%;
height: 100%;
max-height: none;
filter: none;
}
.svg-wrapper.is-top2 :deep(#shape1) {
fill: #211677;
stroke: none;
}
.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;
}
: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;
}
.svg-wrapper {
padding: 20px;
}
}
/* 手机适配 */
@media (max-width: 768px) {
.monitor-container {
gap: 15px;
padding: 10px;
}
.title {
font-size: 1.5rem;
padding: 12px 16px;
letter-spacing: 0.5px;
border-radius: 15px;
}
.svg-wrapper {
padding: 15px;
border-radius: 15px;
}
}
/* 小屏手机适配 */
@media (max-width: 480px) {
.monitor-container {
gap: 10px;
padding: 8px;
}
.title {
font-size: 1.2rem;
padding: 10px 12px;
letter-spacing: 0.3px;
border-radius: 12px;
}
.svg-wrapper {
padding: 10px;
border-radius: 12px;
}
}
</style>