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:
@@ -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>
|
||||
Reference in New Issue
Block a user