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>
161 lines
5.1 KiB
Vue
161 lines
5.1 KiB
Vue
<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>
|