feat: 添加杆塔几何结构可视化组件
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
version: 1.0.14
|
version: 1.0.16
|
||||||
company_name: EGM
|
company_name: EGM
|
||||||
file_description: EGM Lightning Protection Calculator
|
file_description: EGM Lightning Protection Calculator
|
||||||
product_name: Lightening
|
product_name: Lightening
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>EGM 输电线路绕击跳闸率计算 v1.0.14</title>
|
<title>EGM 输电线路绕击跳闸率计算 v1.0.16</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<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>
|
||||||
</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-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
@@ -407,6 +418,7 @@ import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
|
|||||||
import type { AllParameters } from '@/types'
|
import type { AllParameters } from '@/types'
|
||||||
import LogComponent from './Log.vue'
|
import LogComponent from './Log.vue'
|
||||||
import Animation from './Animation.vue'
|
import Animation from './Animation.vue'
|
||||||
|
import Geometry from './Geometry.vue'
|
||||||
|
|
||||||
// 默认参数
|
// 默认参数
|
||||||
const defaultParams: AllParameters = {
|
const defaultParams: AllParameters = {
|
||||||
|
|||||||
Reference in New Issue
Block a user