Files
geo-tools/src/views/BearingCalculator.vue
T
missumandCursor 88e201e50b refactor: harden deps, fix geo math, and add engineering tooling
Replace xlsx with exceljs, unify CRS/DMS logic, move pages to views with shared map/toast composables, and add Vitest/ESLint/Prettier plus docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 09:26:15 +08:00

244 lines
6.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="container">
<div class="tool-header">
<router-link to="/" class="back-link"> 返回首页</router-link>
<h2>🧭 方位角计算工具</h2>
<p>根据两点坐标计算方位角和象限角</p>
</div>
<div class="card">
<div class="grid grid-2">
<div>
<h3 class="card-title">起点坐标</h3>
<div class="input-group">
<label class="input-label">X坐标或经度</label>
<input type="number" v-model.number="point1.x" class="input" step="0.0001" placeholder="例:0">
</div>
<div class="input-group">
<label class="input-label">Y坐标或纬度</label>
<input type="number" v-model.number="point1.y" class="input" step="0.0001" placeholder="例:0">
</div>
</div>
<div>
<h3 class="card-title">终点坐标</h3>
<div class="input-group">
<label class="input-label">X坐标或经度</label>
<input type="number" v-model.number="point2.x" class="input" step="0.0001" placeholder="例:100">
</div>
<div class="input-group">
<label class="input-label">Y坐标或纬度</label>
<input type="number" v-model.number="point2.y" class="input" step="0.0001" placeholder="例:100">
</div>
</div>
</div>
<button @click="calculate" class="btn btn-primary mt-lg">计算方位角</button>
<div v-if="result" class="result mt-xl">
<div class="result-label">计算结果</div>
<div class="bearing-display">
<div class="bearing-circle">
<div class="compass-marker north">N</div>
<div class="compass-marker east">E</div>
<div class="compass-marker south">S</div>
<div class="compass-marker west">W</div>
<div class="bearing-arrow" :style="{ transform: `rotate(${result.azimuth}deg)` }"></div>
</div>
<div class="bearing-values">
<div class="value-item">
<span class="value-label">真方位角</span>
<span class="value-number">{{ result.azimuth.toFixed(4) }}°</span>
</div>
<div class="value-item">
<span class="value-label">象限角</span>
<span class="value-number">{{ result.quadrant }}</span>
</div>
</div>
</div>
<div class="info-box mt-lg">
<p><strong>说明</strong></p>
<p>真方位角从正北方向顺时针旋转到目标方向的角度0°-360°)</p>
<p>象限角从南北方向量起向东或向西的角度</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { calculateBearing } from '../utils/geometry'
const point1 = ref({ x: 0, y: 0 })
const point2 = ref({ x: 100, y: 100 })
const result = ref(null)
const calculate = () => {
result.value = calculateBearing(
point1.value.x,
point1.value.y,
point2.value.x,
point2.value.y
)
}
</script>
<style scoped>
.tool-header {
text-align: center;
margin-bottom: var(--spacing-xl);
}
.back-link {
display: inline-block;
color: var(--text-secondary);
text-decoration: none;
margin-bottom: var(--spacing-md);
transition: color var(--transition-base);
}
.back-link:hover {
color: var(--primary);
}
.bearing-display {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-xl);
align-items: center;
margin-top: var(--spacing-lg);
}
.bearing-circle {
position: relative;
width: 200px;
height: 200px;
margin: 0 auto;
border: 3px solid var(--border-color);
border-radius: 50%;
background: var(--bg-secondary);
}
.compass-marker {
position: absolute;
font-weight: 700;
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.compass-marker.north {
top: 10px;
left: 50%;
transform: translateX(-50%);
color: var(--danger);
}
.compass-marker.east {
right: 10px;
top: 50%;
transform: translateY(-50%);
}
.compass-marker.south {
bottom: 10px;
left: 50%;
transform: translateX(-50%);
}
.compass-marker.west {
left: 10px;
top: 50%;
transform: translateY(-50%);
}
.bearing-arrow {
position: absolute;
top: 50%;
left: 50%;
width: 4px;
height: 80px;
background: var(--gradient-primary);
transform-origin: bottom center;
margin-left: -2px;
margin-top: -80px;
border-radius: 2px;
transition: transform 0.5s ease;
}
.bearing-arrow::before {
content: '';
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 12px solid var(--primary);
}
.bearing-values {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.value-item {
display: flex;
flex-direction: column;
padding: var(--spacing-md);
background: var(--bg-secondary);
border-radius: var(--border-radius-sm);
}
.value-label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
}
.value-number {
font-size: var(--font-size-2xl);
font-weight: 700;
background: var(--gradient-primary);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.info-box {
padding: var(--spacing-lg);
background: var(--bg-secondary);
border-radius: var(--border-radius-sm);
border-left: 4px solid var(--primary);
}
.info-box p {
margin-bottom: var(--spacing-xs);
font-size: var(--font-size-sm);
color: var(--text-secondary);
line-height: 1.6;
}
.info-box p:first-child {
font-weight: 600;
color: var(--text-primary);
}
@media (max-width: 768px) {
.bearing-display {
grid-template-columns: 1fr;
}
.bearing-circle {
width: 180px;
height: 180px;
}
}
</style>