|
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> |
|||