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>
This commit is contained in:
2026-08-12 09:26:15 +08:00
co-authored by Cursor
parent be48495dcf
commit 88e201e50b
29 changed files with 3270 additions and 2756 deletions
+243
View File
@@ -0,0 +1,243 @@
<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>