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
+160
View File
@@ -0,0 +1,160 @@
<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="input-group">
<label class="input-label">坐标类型</label>
<div class="radio-group">
<label>
<input type="radio" v-model="coordType" value="geographic" name="coordType">
经纬度坐标
</label>
<label>
<input type="radio" v-model="coordType" value="planar" name="coordType">
平面坐标
</label>
</div>
</div>
<div class="grid grid-2">
<div>
<h3 class="card-title">起点坐标</h3>
<div class="input-group">
<label class="input-label">{{ coordType === 'geographic' ? '纬度' : 'Y坐标' }}</label>
<input type="number" v-model.number="point1.lat" class="input" step="0.000001"
placeholder="例:39.9075">
</div>
<div class="input-group">
<label class="input-label">{{ coordType === 'geographic' ? '经度' : 'X坐标' }}</label>
<input type="number" v-model.number="point1.lon" class="input" step="0.000001"
placeholder="例:116.3972">
</div>
</div>
<div>
<h3 class="card-title">终点坐标</h3>
<div class="input-group">
<label class="input-label">{{ coordType === 'geographic' ? '纬度' : 'Y坐标' }}</label>
<input type="number" v-model.number="point2.lat" class="input" step="0.000001"
placeholder="例:31.2304">
</div>
<div class="input-group">
<label class="input-label">{{ coordType === 'geographic' ? '经度' : 'X坐标' }}</label>
<input type="number" v-model.number="point2.lon" class="input" step="0.000001"
placeholder="例:121.4737">
</div>
</div>
</div>
<button @click="calculate" class="btn btn-primary mt-lg">计算距离</button>
<div v-if="distance !== null" class="result mt-xl">
<div class="result-label">计算结果</div>
<div class="result-value">
{{ formatDistance(distance) }}
</div>
<div class="result-details mt-md">
<p v-if="coordType === 'geographic'">
<strong></strong>{{ distance.toFixed(2) }} m<br>
<strong>千米</strong>{{ (distance / 1000).toFixed(3) }} km
</p>
<p v-else>
<strong>距离</strong>{{ distance.toFixed(4) }} 单位
</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { haversineDistance, planarDistance } from '../utils/coordinate'
const coordType = ref('geographic')
const point1 = ref({ lat: 39.9075, lon: 116.3972 })
const point2 = ref({ lat: 31.2304, lon: 121.4737 })
const distance = ref(null)
const calculate = () => {
if (coordType.value === 'geographic') {
distance.value = haversineDistance(
point1.value.lat,
point1.value.lon,
point2.value.lat,
point2.value.lon
)
} else {
distance.value = planarDistance(
point1.value.lon,
point1.value.lat,
point2.value.lon,
point2.value.lat
)
}
}
const formatDistance = (dist) => {
if (coordType.value === 'geographic') {
if (dist < 1000) {
return `${dist.toFixed(2)}`
} else {
return `${(dist / 1000).toFixed(3)} 千米`
}
} else {
return `${dist.toFixed(4)}`
}
}
</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);
}
.radio-group {
display: flex;
gap: var(--spacing-lg);
}
.radio-group label {
display: flex;
align-items: center;
gap: var(--spacing-xs);
cursor: pointer;
font-size: var(--font-size-base);
color: var(--text-primary);
}
.radio-group input[type="radio"] {
cursor: pointer;
}
.result-details {
font-size: var(--font-size-base);
color: var(--text-secondary);
line-height: 1.8;
}
.result-details p {
margin: 0;
}
</style>