feat: 添加杆塔几何结构可视化组件
This commit is contained in:
@@ -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>
|
||||
|
||||
320
webui/src/components/Geometry.vue
Normal file
320
webui/src/components/Geometry.vue
Normal 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>
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user