first commit
This commit is contained in:
493
src/App.vue
Normal file
493
src/App.vue
Normal file
@@ -0,0 +1,493 @@
|
||||
<template>
|
||||
<div class="monitor-container">
|
||||
<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" 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;
|
||||
}
|
||||
|
||||
.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>
|
||||
Reference in New Issue
Block a user