|
After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 467 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 467 KiB |
@ -0,0 +1,135 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { onMounted, onUnmounted, ref, watch } from 'vue' |
||||
|
import { Card } from 'ant-design-vue' |
||||
|
import Svg from './svg.vue' |
||||
|
|
||||
|
const props = defineProps({ |
||||
|
data: { |
||||
|
type: Object, |
||||
|
default: () => {}, |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
// 模拟实时数据 |
||||
|
const realtimeData = ref({ |
||||
|
path: '/送风机.svg', |
||||
|
// Header: 'SVG实时数据可视化示例', |
||||
|
realtimeValues: { |
||||
|
'current-value': 0, |
||||
|
'status-text': '状态: 正常', |
||||
|
'status-indicator': 'normal', |
||||
|
}, |
||||
|
valueMapping: { |
||||
|
'current-value': 'current-value', |
||||
|
'status-text': 'status-text', |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
// 模拟数据更新的定时器 |
||||
|
let updateTimer: number | null = null |
||||
|
|
||||
|
// 生成随机数据更新 |
||||
|
function updateData() { |
||||
|
// 生成0-100的随机值 |
||||
|
const newValue = Math.floor(Math.random() * 101) |
||||
|
|
||||
|
// // 根据值设置状态 |
||||
|
// let status = '正常' |
||||
|
// let statusColor = '#52c41a' |
||||
|
|
||||
|
// if (newValue > 90) { |
||||
|
// status = '危险' |
||||
|
// statusColor = '#ff4d4f' |
||||
|
// } |
||||
|
// else if (newValue > 70) { |
||||
|
// status = '警告' |
||||
|
// statusColor = '#faad14' |
||||
|
// } |
||||
|
|
||||
|
// 更新数据 |
||||
|
realtimeData.value.realtimeValues = { |
||||
|
'current-value': newValue, |
||||
|
// 'status-text': `状态: ${status}`, |
||||
|
// 'status-indicator': status.toLowerCase(), |
||||
|
} |
||||
|
|
||||
|
// 更新指针角度(0-100映射到-135到135度) |
||||
|
const angle = -135 + (newValue / 100) * 270 |
||||
|
const svgElement = document.querySelector('.svg-container > svg') |
||||
|
// svgElement?.removeAttribute('width') |
||||
|
// svgElement?.removeAttribute('height') |
||||
|
if (svgElement) { |
||||
|
const pointer = svgElement.getElementById('pointer') |
||||
|
if (pointer) |
||||
|
pointer.setAttribute('transform', `rotate(${angle} 200 150)`) |
||||
|
|
||||
|
// 更新状态指示器颜色 |
||||
|
const statusIndicator = svgElement.getElementById('status-indicator') |
||||
|
if (statusIndicator) |
||||
|
statusIndicator.setAttribute('fill', statusColor) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 生命周期钩子 |
||||
|
onMounted(() => { |
||||
|
// 立即更新一次 |
||||
|
updateData() |
||||
|
|
||||
|
// 设置定时器,每2秒更新一次数据 |
||||
|
updateTimer = window.setInterval(updateData, 2000) |
||||
|
}) |
||||
|
watch( |
||||
|
() => props.data, |
||||
|
(newValue, oldValue) => { |
||||
|
// 下方信息不会打印在控制台中 |
||||
|
console.log('a has changed', newValue, oldValue) |
||||
|
realtimeData.value.path = props.data.path |
||||
|
}, |
||||
|
) |
||||
|
onUnmounted(() => { |
||||
|
// 清理定时器 |
||||
|
if (updateTimer) |
||||
|
clearInterval(updateTimer) |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<!-- <div class="svg-demo-container"> --> |
||||
|
<!-- <div class="demo-header"> |
||||
|
<h2>SVG实时数据可视化演示</h2> |
||||
|
<p>这个演示展示了如何嵌入SVG并实时更新其中的数据。数据每2秒自动更新一次,也可以点击下方按钮手动更新。</p> |
||||
|
<button |
||||
|
class="update-btn" |
||||
|
@click="manualUpdate" |
||||
|
> |
||||
|
手动更新数据 |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<div class="demo-content"> |
||||
|
<Svg :data="realtimeData" /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="demo-info"> |
||||
|
<h3>使用说明</h3> |
||||
|
<ul> |
||||
|
<li>1. 将SVG文件放在项目的assets/svg目录下</li> |
||||
|
<li>2. 在父组件中准备数据对象,包含SVG路径、实时值和映射关系</li> |
||||
|
<li>3. SVG中的元素需要有唯一的ID,以便与数据进行绑定</li> |
||||
|
<li>4. 组件支持自动监听数据变化并更新SVG内容</li> |
||||
|
<li>5. 可以通过点击SVG区域手动触发数据更新</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> --> |
||||
|
<!-- </div> --> |
||||
|
<Card class="enter-y !my-1" :title="props.data.Header"> |
||||
|
<!-- <img class="mx-auto h-full w-full" :src="props.data.path"> --> |
||||
|
<Svg class="h-full w-full" :data="realtimeData" /> |
||||
|
</Card> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
::v-deep .ant-card-body { |
||||
|
padding: 10px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,225 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { Card } from 'ant-design-vue' |
||||
|
import { nextTick, onMounted, ref, toRefs, watch } from 'vue' |
||||
|
|
||||
|
interface SvgData { |
||||
|
path: string // SVG文件路径 |
||||
|
Header?: string // 卡片标题 |
||||
|
realtimeValues?: Record<string, any> // 实时数据对象 |
||||
|
valueMapping?: Record<string, string> // SVG元素ID到数据字段的映射 |
||||
|
} |
||||
|
|
||||
|
const props = defineProps<{ |
||||
|
data: SvgData |
||||
|
}>() |
||||
|
|
||||
|
const { data } = toRefs(props) |
||||
|
const svgContent = ref<string>('') |
||||
|
const isLoading = ref<boolean>(true) |
||||
|
const svgError = ref<string>('') |
||||
|
|
||||
|
// 加载SVG文件 |
||||
|
async function loadSvg() { |
||||
|
if (!data.value.path) { |
||||
|
svgError.value = '未提供SVG路径' |
||||
|
isLoading.value = false |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
isLoading.value = true |
||||
|
svgError.value = '' |
||||
|
|
||||
|
// 判断路径是否为完整URL |
||||
|
let url = data.value.path |
||||
|
if (!url.startsWith('http') && !url.startsWith('/')) { |
||||
|
// 如果是相对路径,尝试从assets加载 |
||||
|
url = new URL(`../../../../assets/${url}`, import.meta.url).href |
||||
|
} |
||||
|
|
||||
|
const response = await fetch(url) |
||||
|
if (!response.ok) |
||||
|
throw new Error(`无法加载SVG: ${response.statusText}`) |
||||
|
|
||||
|
svgContent.value = await response.text() |
||||
|
|
||||
|
// 等待DOM更新后应用实时数据 |
||||
|
await nextTick() |
||||
|
|
||||
|
// 处理SVG的viewBox和尺寸属性以实现自适应 |
||||
|
const svgElement = document.querySelector('.svg-container > svg') |
||||
|
if (svgElement) { |
||||
|
// 移除固定的width和height属性,让SVG由CSS控制大小 |
||||
|
svgElement.removeAttribute('width') |
||||
|
svgElement.removeAttribute('height') |
||||
|
|
||||
|
// 确保SVG有viewBox属性 |
||||
|
if (!svgElement.hasAttribute('viewBox')) { |
||||
|
// 如果没有viewBox,尝试根据SVG内容创建一个 |
||||
|
const bbox = svgElement.getBBox() |
||||
|
if (bbox.width && bbox.height) |
||||
|
svgElement.setAttribute('viewBox', `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`) |
||||
|
} |
||||
|
|
||||
|
// 设置preserveAspectRatio属性以保持宽高比 |
||||
|
svgElement.setAttribute('preserveAspectRatio', 'xMidYMid meet') |
||||
|
} |
||||
|
|
||||
|
updateRealtimeValues() |
||||
|
} |
||||
|
catch (error) { |
||||
|
console.error('加载SVG失败:', error) |
||||
|
svgError.value = `加载失败: ${error instanceof Error ? error.message : String(error)}` |
||||
|
} |
||||
|
finally { |
||||
|
isLoading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 更新SVG中的实时数据 |
||||
|
function updateRealtimeValues() { |
||||
|
if (!svgContent.value || !data.value.realtimeValues) |
||||
|
return |
||||
|
|
||||
|
try { |
||||
|
// 获取SVG元素 |
||||
|
const svgElement = document.querySelector('.svg-container > svg') |
||||
|
console.log(svgElement) |
||||
|
if (!svgElement) |
||||
|
return |
||||
|
|
||||
|
const { realtimeValues, valueMapping = {} } = data.value |
||||
|
|
||||
|
// 遍历映射关系,更新SVG元素 |
||||
|
Object.entries(valueMapping).forEach(([svgElementId, dataField]) => { |
||||
|
console.log([svgElementId, dataField]) |
||||
|
const element = svgElement.getElementById(svgElementId) |
||||
|
console.log(element) |
||||
|
|
||||
|
if (element && realtimeValues[dataField] !== undefined) { |
||||
|
// 根据元素类型设置不同的属性 |
||||
|
if (element.tagName === 'text' || element.tagName === 'tspan') { |
||||
|
element.textContent = String(realtimeValues[dataField]) |
||||
|
} |
||||
|
else if (element.tagName === 'circle' || element.tagName === 'rect' || element.tagName === 'path') { |
||||
|
// 可以根据需要设置其他属性,比如颜色、宽度等 |
||||
|
element.setAttribute('data-value', String(realtimeValues[dataField])) |
||||
|
// 示例:根据数值设置颜色 |
||||
|
if (typeof realtimeValues[dataField] === 'number') { |
||||
|
const value = realtimeValues[dataField] as number |
||||
|
// 简单的颜色映射示例 |
||||
|
if (value > 90) |
||||
|
element.setAttribute('fill', '#ff4d4f') |
||||
|
else if (value > 70) |
||||
|
element.setAttribute('fill', '#faad14') |
||||
|
else |
||||
|
element.setAttribute('fill', '#52c41a') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
// 如果没有映射配置,尝试直接匹配元素ID和数据字段 |
||||
|
if (Object.keys(valueMapping).length === 0) { |
||||
|
Object.entries(realtimeValues).forEach(([field, value]) => { |
||||
|
const element = svgElement.getElementById(field) |
||||
|
if (element) { |
||||
|
if (element.tagName === 'text' || element.tagName === 'tspan') |
||||
|
element.textContent = String(value) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
catch (error) { |
||||
|
console.error('更新实时数据失败:', error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 监听数据变化 |
||||
|
watch( |
||||
|
() => data.value.path, |
||||
|
() => { |
||||
|
loadSvg() |
||||
|
}, |
||||
|
{ immediate: true }, |
||||
|
) |
||||
|
|
||||
|
// 监听实时数据变化 |
||||
|
watch( |
||||
|
() => data.value.realtimeValues, |
||||
|
() => { |
||||
|
updateRealtimeValues() |
||||
|
}, |
||||
|
{ deep: true }, |
||||
|
) |
||||
|
|
||||
|
onMounted(() => { |
||||
|
loadSvg() |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<!-- <Card class="enter-y !my-1" :title="data.Header || 'SVG可视化'"> --> |
||||
|
<div v-if="isLoading" class="h-40 flex items-center justify-center"> |
||||
|
加载中... |
||||
|
</div> |
||||
|
<div v-else-if="svgError" class="h-40 flex items-center justify-center text-red-500"> |
||||
|
{{ svgError }} |
||||
|
</div> |
||||
|
<div v-else class="svg-container" @click="updateRealtimeValues" v-html="svgContent" /> |
||||
|
<!-- </Card> --> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* 响应式调整 */ |
||||
|
@media (max-width: 768px) { |
||||
|
.svg-container { |
||||
|
min-height: 300px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
::v-deep .ant-card-body { |
||||
|
padding: 10px; |
||||
|
overflow: auto; |
||||
|
} |
||||
|
|
||||
|
vg-container { |
||||
|
/* 响应式容器设置 */ |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
min-height: 400px; |
||||
|
overflow: visible; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.svg-container > svg { |
||||
|
box-sizing: border-box !important; |
||||
|
display: block !important; |
||||
|
|
||||
|
/* 核心自适应设置 - 配合viewBox使用 */ |
||||
|
width: 100% !important; |
||||
|
|
||||
|
/* 确保SVG完全响应容器变化 */ |
||||
|
max-width: none !important; |
||||
|
height: auto !important; |
||||
|
max-height: none !important; |
||||
|
padding: 0 !important; |
||||
|
|
||||
|
/* 消除默认样式 */ |
||||
|
margin: 0 !important; |
||||
|
|
||||
|
/* 确保SVG内容完全可见 */ |
||||
|
overflow: visible !important; |
||||
|
border: none !important; |
||||
|
} |
||||
|
|
||||
|
/* 确保所有父容器也支持响应式 */ |
||||
|
:deep(.ant-card), |
||||
|
:deep(.ant-card-body) { |
||||
|
width: 100%; |
||||
|
height: auto; |
||||
|
padding: 0; |
||||
|
margin: 0; |
||||
|
} |
||||
|
</style> |
||||