feat: 添加杆塔几何结构可视化组件

This commit is contained in:
dmy
2026-03-04 09:13:51 +08:00
parent 7f4a6751b4
commit 4b75c6a521
5 changed files with 335 additions and 3 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EGM 输电线路绕击跳闸率计算 v1.0.14</title>
<title>EGM 输电线路绕击跳闸率计算 v1.0.16</title>
</head>
<body>
<div id="app"></div>

View File

@@ -0,0 +1,320 @@
<template>
<q-card class="shadow-2">
<q-card-section class="bg-indigo-50 cursor-pointer" @click="toggleExpand">
<div class="text-h6 text-indigo-900 flex items-center gap-2">
<q-icon name="architecture" />
杆塔几何结构
<q-space />
<q-icon :name="expanded ? 'expand_less' : 'expand_more'" />
</div>
</q-card-section>
<q-slide-transition>
<q-card-section v-show="expanded">
<div class="geometry-container">
<canvas
ref="canvasRef"
:width="canvasWidth"
:height="canvasHeight"
class="geometry-canvas"
/>
<!-- 图例 -->
<div class="legend q-mt-sm">
<div class="row q-gutter-md justify-center">
<div class="flex items-center gap-1">
<div class="legend-color" style="background: #4CAF50;"></div>
<span class="text-caption">地线</span>
</div>
<div class="flex items-center gap-1">
<div class="legend-color" style="background: #FF9800;"></div>
<span class="text-caption">导线</span>
</div>
<div class="flex items-center gap-1">
<div class="legend-color" style="background: #795548;"></div>
<span class="text-caption">地面</span>
</div>
</div>
</div>
</div>
</q-card-section>
</q-slide-transition>
</q-card>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
// Props
const props = defineProps<{
hArm: number[] // 导、地线挂点垂直坐标 [地线, 导线1, ...]
gcX: number[] // 导、地线水平坐标 [地线, 导线1, ...]
hCSag: number // 导线弧垂
hGSag: number // 地线弧垂
groundAngels: number[] // 地面倾角
}>()
// Canvas 尺寸
const canvasWidth = 600
const canvasHeight = 500
// 展开/折叠状态
const expanded = ref(true)
const canvasRef = ref<HTMLCanvasElement | null>(null)
let ctx: CanvasRenderingContext2D | null = null
// 计算参数
const margin = { top: 40, right: 40, bottom: 60, left: 60 }
const plotWidth = canvasWidth - margin.left - margin.right
const plotHeight = canvasHeight - margin.top - margin.bottom
// 切换展开/折叠
const toggleExpand = () => {
expanded.value = !expanded.value
if (expanded.value) {
setTimeout(() => {
if (canvasRef.value) {
ctx = canvasRef.value.getContext('2d')
draw()
}
}, 350)
}
}
// 计算坐标范围
const calculateRange = () => {
const allHeights = [...props.hArm, 0] // 包含地面
const allX = [...props.gcX, -props.gcX[0] * 0.5, props.gcX[0] * 1.5] // 扩展水平范围
const yMin = 0
const yMax = Math.max(...allHeights) * 1.15
const xMin = Math.min(...allX) * 1.2
const xMax = Math.max(...allX) * 1.2
return { xMin, xMax, yMin, yMax }
}
// 坐标转换:数据坐标 -> Canvas 坐标
const toCanvasX = (x: number, range: ReturnType<typeof calculateRange>): number => {
return margin.left + ((x - range.xMin) / (range.xMax - range.xMin)) * plotWidth
}
const toCanvasY = (y: number, range: ReturnType<typeof calculateRange>): number => {
// Canvas Y 轴向下,需要反转
return margin.top + plotHeight - ((y - range.yMin) / (range.yMax - range.yMin)) * plotHeight
}
// 绘制函数
const draw = () => {
if (!ctx || !expanded.value) return
const range = calculateRange()
// 清除画布
ctx.fillStyle = '#fafafa'
ctx.fillRect(0, 0, canvasWidth, canvasHeight)
// 绘制背景网格
ctx.strokeStyle = '#e8e8e8'
ctx.lineWidth = 1
// 垂直网格线
const xStep = Math.ceil((range.xMax - range.xMin) / 10)
for (let x = Math.ceil(range.xMin / xStep) * xStep; x <= range.xMax; x += xStep) {
ctx.beginPath()
ctx.moveTo(toCanvasX(x, range), margin.top)
ctx.lineTo(toCanvasX(x, range), margin.top + plotHeight)
ctx.stroke()
}
// 水平网格线
const yStep = Math.ceil((range.yMax - range.yMin) / 8)
for (let y = Math.ceil(range.yMin / yStep) * yStep; y <= range.yMax; y += yStep) {
ctx.beginPath()
ctx.moveTo(margin.left, toCanvasY(y, range))
ctx.lineTo(margin.left + plotWidth, toCanvasY(y, range))
ctx.stroke()
}
// 绘制坐标轴
ctx.strokeStyle = '#333'
ctx.lineWidth = 1.5
// Y 轴
ctx.beginPath()
ctx.moveTo(margin.left, margin.top)
ctx.lineTo(margin.left, margin.top + plotHeight)
ctx.stroke()
// X 轴(地面)
ctx.beginPath()
ctx.moveTo(margin.left, toCanvasY(0, range))
ctx.lineTo(margin.left + plotWidth, toCanvasY(0, range))
ctx.stroke()
// 绘制刻度标签
ctx.fillStyle = '#666'
ctx.font = '11px Arial'
// Y 轴刻度
ctx.textAlign = 'right'
for (let y = yStep; y <= range.yMax; y += yStep) {
const canvasY = toCanvasY(y, range)
ctx.fillText(`${y}`, margin.left - 8, canvasY + 4)
// 刻度线
ctx.beginPath()
ctx.moveTo(margin.left - 4, canvasY)
ctx.lineTo(margin.left, canvasY)
ctx.stroke()
}
// X 轴刻度
ctx.textAlign = 'center'
for (let x = Math.ceil(range.xMin / xStep) * xStep; x <= range.xMax; x += xStep) {
if (x !== 0) {
const canvasX = toCanvasX(x, range)
ctx.fillText(`${x}`, canvasX, toCanvasY(0, range) + 18)
// 刻度线
ctx.beginPath()
ctx.moveTo(canvasX, toCanvasY(0, range))
ctx.lineTo(canvasX, toCanvasY(0, range) + 4)
ctx.stroke()
}
}
// 轴标签
ctx.font = '12px Arial'
ctx.fillStyle = '#333'
ctx.textAlign = 'center'
ctx.fillText('水平距离 (m)', margin.left + plotWidth / 2, canvasHeight - 10)
ctx.save()
ctx.translate(15, margin.top + plotHeight / 2)
ctx.rotate(-Math.PI / 2)
ctx.fillText('高度 (m)', 0, 0)
ctx.restore()
// 绘制地面倾角
drawGround(range)
// 绘制导线和地线挂点
drawWirePoints(range)
}
// 绘制导线和地线挂点
const drawWirePoints = (range: ReturnType<typeof calculateRange>) => {
if (!ctx) return
const c = ctx
props.hArm.forEach((height, index) => {
const wireX = props.gcX[index] || 0
const isGroundWire = index === 0
const canvasX = toCanvasX(wireX, range)
const canvasY = toCanvasY(height, range)
// 绘制挂点标记
c.fillStyle = isGroundWire ? '#4CAF50' : '#FF9800'
c.beginPath()
c.arc(canvasX, canvasY, 8, 0, Math.PI * 2)
c.fill()
// 标注信息
c.fillStyle = '#333'
c.font = 'bold 11px Arial'
c.textAlign = 'left'
const labelX = canvasX + 12
const labelY = canvasY - 8
const wireName = isGroundWire ? '地线' : `导线${index}`
const heightLabel = `H=${height}m`
const xLabel = `X=${wireX}m`
c.fillText(wireName, labelX, labelY)
c.font = '10px Arial'
c.fillStyle = '#666'
c.fillText(heightLabel, labelX, labelY + 14)
c.fillText(xLabel, labelX, labelY + 26)
})
}
// 绘制地面
const drawGround = (range: ReturnType<typeof calculateRange>) => {
if (!ctx) return
const groundAngle = props.groundAngels[0] || 0
const angleRad = (groundAngle * Math.PI) / 180
ctx.strokeStyle = '#795548'
ctx.lineWidth = 2
// 地面线(考虑倾角)
const groundLength = range.xMax - range.xMin
const dy = Math.tan(angleRad) * groundLength
const leftX = range.xMin
const rightX = range.xMax
const leftY = groundAngle >= 0 ? dy : 0
const rightY = groundAngle >= 0 ? 0 : -dy
ctx.beginPath()
ctx.moveTo(toCanvasX(leftX, range), toCanvasY(leftY, range))
ctx.lineTo(toCanvasX(rightX, range), toCanvasY(rightY, range))
ctx.stroke()
// 地面填充
ctx.fillStyle = 'rgba(121, 85, 72, 0.1)'
ctx.beginPath()
ctx.moveTo(toCanvasX(leftX, range), toCanvasY(leftY, range))
ctx.lineTo(toCanvasX(rightX, range), toCanvasY(rightY, range))
ctx.lineTo(toCanvasX(rightX, range), toCanvasY(-50, range))
ctx.lineTo(toCanvasX(leftX, range), toCanvasY(-50, range))
ctx.closePath()
ctx.fill()
}
// 监听参数变化
watch(
() => [props.hArm, props.gcX, props.hCSag, props.hGSag, props.groundAngels],
() => {
draw()
},
{ deep: true }
)
// 初始化
onMounted(() => {
if (canvasRef.value) {
ctx = canvasRef.value.getContext('2d')
draw()
}
})
</script>
<style scoped>
.geometry-container {
display: flex;
flex-direction: column;
align-items: center;
}
.geometry-canvas {
border: 1px solid #e0e0e0;
border-radius: 4px;
background: #fafafa;
}
.legend-color {
width: 16px;
height: 4px;
border-radius: 2px;
}
.legend {
padding: 8px;
background: #f5f5f5;
border-radius: 4px;
}
</style>

View File

@@ -196,6 +196,17 @@
</div>
</div>
</div>
<!-- 杆塔几何结构可视化 -->
<div class="q-mt-md">
<Geometry
:h-arm="params.parameter.h_arm"
:gc-x="params.parameter.gc_x"
:h-c-sag="params.parameter.h_c_sag"
:h-g-sag="params.parameter.h_g_sag"
:ground-angels="params.parameter.ground_angels"
/>
</div>
</q-card-section>
</q-card>
@@ -407,6 +418,7 @@ import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
import type { AllParameters } from '@/types'
import LogComponent from './Log.vue'
import Animation from './Animation.vue'
import Geometry from './Geometry.vue'
// 默认参数
const defaultParams: AllParameters = {