feat: Add AreaCalculator and CoordinateConverter components, introducing @turf/turf, gcoord, and lucide-vue-next dependencies.
This commit is contained in:
@@ -1,82 +1,111 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="tool-header">
|
||||
<router-link to="/" class="back-link">← 返回首页</router-link>
|
||||
<h2>🌍 坐标转换工具</h2>
|
||||
<p>经纬度格式转换:度分秒 ↔ 十进制度数</p>
|
||||
<router-link to="/" class="back-link">
|
||||
<ChevronLeft class="icon-sm" /> 返回首页
|
||||
</router-link>
|
||||
<h2>
|
||||
<Globe class="icon-lg" /> 坐标转换工具
|
||||
</h2>
|
||||
<p>支持经纬度格式转换及常用地图坐标系互转 (WGS84, GCJ02, BD09)</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid grid-2">
|
||||
<!-- DMS to Decimal -->
|
||||
<div>
|
||||
<h3 class="card-title">度分秒 → 十进制</h3>
|
||||
<div class="grid grid-2 gap-xl">
|
||||
<!-- Format Conversion -->
|
||||
<div class="card">
|
||||
<h3 class="card-title">
|
||||
<RefreshCw class="icon-md" /> 格式转换 (DMS ↔ Decimal)
|
||||
</h3>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">纬度(度分秒)</label>
|
||||
<div class="dms-input">
|
||||
<input type="number" v-model.number="dms.lat.degrees" class="input" placeholder="度">
|
||||
<input type="number" v-model.number="dms.lat.minutes" class="input" placeholder="分">
|
||||
<input type="number" v-model.number="dms.lat.seconds" class="input" placeholder="秒">
|
||||
<select v-model="dms.lat.direction" class="input">
|
||||
<option value="N">N</option>
|
||||
<option value="S">S</option>
|
||||
</select>
|
||||
<div class="grid grid-2 gap-md">
|
||||
<div>
|
||||
<div class="input-group">
|
||||
<label class="input-label">度分秒 → 十进制</label>
|
||||
<div class="dms-input">
|
||||
<input type="number" v-model.number="dms.lat.degrees" class="input" placeholder="度">
|
||||
<input type="number" v-model.number="dms.lat.minutes" class="input" placeholder="分">
|
||||
<input type="number" v-model.number="dms.lat.seconds" class="input" placeholder="秒">
|
||||
<select v-model="dms.lat.direction" class="input">
|
||||
<option value="N">N</option>
|
||||
<option value="S">S</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="convertToDecimal" class="btn btn-primary btn-full mt-md">转换为十进制</button>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">经度(度分秒)</label>
|
||||
<div class="dms-input">
|
||||
<input type="number" v-model.number="dms.lon.degrees" class="input" placeholder="度">
|
||||
<input type="number" v-model.number="dms.lon.minutes" class="input" placeholder="分">
|
||||
<input type="number" v-model.number="dms.lon.seconds" class="input" placeholder="秒">
|
||||
<select v-model="dms.lon.direction" class="input">
|
||||
<option value="E">E</option>
|
||||
<option value="W">W</option>
|
||||
</select>
|
||||
<div>
|
||||
<div class="input-group">
|
||||
<label class="input-label">十进制 → 度分秒</label>
|
||||
<div class="flex gap-xs">
|
||||
<input type="number" v-model.number="decimal.lat" class="input" placeholder="纬度">
|
||||
<input type="number" v-model.number="decimal.lon" class="input" placeholder="经度">
|
||||
</div>
|
||||
</div>
|
||||
<button @click="convertToDms" class="btn btn-primary btn-full mt-md">转换为度分秒</button>
|
||||
</div>
|
||||
|
||||
<button @click="convertToDecimal" class="btn btn-primary">转换为十进制</button>
|
||||
</div>
|
||||
|
||||
<!-- Decimal to DMS -->
|
||||
<div>
|
||||
<h3 class="card-title">十进制 → 度分秒</h3>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">纬度(十进制)</label>
|
||||
<input type="number" v-model.number="decimal.lat" class="input" placeholder="例:39.9075"
|
||||
step="0.000001">
|
||||
<small class="hint">正数为北纬,负数为南纬</small>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">经度(十进制)</label>
|
||||
<input type="number" v-model.number="decimal.lon" class="input" placeholder="例:116.3972"
|
||||
step="0.000001">
|
||||
<small class="hint">正数为东经,负数为西经</small>
|
||||
</div>
|
||||
|
||||
<button @click="convertToDms" class="btn btn-primary">转换为度分秒</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div v-if="result" class="result mt-xl">
|
||||
<div class="result-label">转换结果</div>
|
||||
<div class="result-content">
|
||||
<div v-if="result.type === 'decimal'">
|
||||
<p><strong>纬度:</strong>{{ result.lat }}°</p>
|
||||
<p><strong>经度:</strong>{{ result.lon }}°</p>
|
||||
<!-- CRS Transformation -->
|
||||
<div class="card">
|
||||
<h3 class="card-title">
|
||||
<MapPin class="icon-md" /> 坐标系转换 (CRS)
|
||||
</h3>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">输入坐标 (经度, 纬度)</label>
|
||||
<input type="text" v-model="crsInput" class="input" placeholder="例: 116.397, 39.908">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2 gap-md">
|
||||
<div class="input-group">
|
||||
<label class="input-label">来源坐标系</label>
|
||||
<select v-model="sourceCrs" class="input">
|
||||
<option value="WGS84">WGS84 (大地坐标)</option>
|
||||
<option value="GCJ02">GCJ02 (火星坐标)</option>
|
||||
<option value="BD09">BD09 (百度坐标)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p><strong>纬度:</strong>{{ result.lat }}</p>
|
||||
<p><strong>经度:</strong>{{ result.lon }}</p>
|
||||
<div class="input-group">
|
||||
<label class="input-label">目标坐标系</label>
|
||||
<select v-model="targetCrs" class="input">
|
||||
<option value="GCJ02">GCJ02 (火星坐标)</option>
|
||||
<option value="WGS84">WGS84 (大地坐标)</option>
|
||||
<option value="BD09">BD09 (百度坐标)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="copyResult" class="btn btn-secondary mt-md">📋 复制结果</button>
|
||||
|
||||
<button @click="transformCrs" class="btn btn-primary btn-full mt-md">执行转换</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Area -->
|
||||
<div v-if="result" class="card mt-xl">
|
||||
<div class="flex justify-between items-center mb-md">
|
||||
<h4 class="m-0 flex items-center gap-xs">
|
||||
<CheckCircle2 class="icon-md text-success" /> 转换结果
|
||||
</h4>
|
||||
<button @click="copyResult" class="btn btn-secondary btn-sm">
|
||||
<Copy class="icon-sm" /> 复制结果
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="result-box">
|
||||
<div v-if="result.type === 'decimal'">
|
||||
<p><strong>纬度 (Lat):</strong> {{ result.lat }}</p>
|
||||
<p><strong>经度 (Lon):</strong> {{ result.lon }}</p>
|
||||
</div>
|
||||
<div v-else-if="result.type === 'dms'">
|
||||
<p><strong>纬度:</strong> {{ result.lat }}</p>
|
||||
<p><strong>经度:</strong> {{ result.lon }}</p>
|
||||
</div>
|
||||
<div v-else-if="result.type === 'crs'">
|
||||
<p><strong>经度 (Lon):</strong> {{ result.lon.toFixed(8) }}</p>
|
||||
<p><strong>纬度 (Lat):</strong> {{ result.lat.toFixed(8) }}</p>
|
||||
<small class="text-tertiary">由 {{ sourceCrs }} 转换为 {{ targetCrs }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,17 +113,21 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import gcoord from 'gcoord'
|
||||
import { ChevronLeft, Globe, RefreshCw, MapPin, Copy, CheckCircle2 } from 'lucide-vue-next'
|
||||
import { dmsToDecimal, decimalToDms, formatCoordinate } from '../utils/coordinate'
|
||||
|
||||
// DMS Conversion State
|
||||
const dms = ref({
|
||||
lat: { degrees: 39, minutes: 54, seconds: 27, direction: 'N' },
|
||||
lon: { degrees: 116, minutes: 23, seconds: 50, direction: 'E' }
|
||||
})
|
||||
const decimal = ref({ lat: 39.9075, lon: 116.3972 })
|
||||
|
||||
const decimal = ref({
|
||||
lat: 39.9075,
|
||||
lon: 116.3972
|
||||
})
|
||||
// CRS Transformation State
|
||||
const crsInput = ref('116.397451, 39.909235')
|
||||
const sourceCrs = ref('WGS84')
|
||||
const targetCrs = ref('GCJ02')
|
||||
|
||||
const result = ref(null)
|
||||
|
||||
@@ -117,10 +150,36 @@ const convertToDms = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const transformCrs = () => {
|
||||
const parts = crsInput.value.split(/[,,\s]+/).map(p => parseFloat(p.trim())).filter(p => !isNaN(p))
|
||||
if (parts.length < 2) {
|
||||
alert('请输入有效的坐标 (经度, 纬度)')
|
||||
return
|
||||
}
|
||||
|
||||
const [lon, lat] = parts
|
||||
const transformed = gcoord.transform(
|
||||
[lon, lat],
|
||||
gcoord[sourceCrs.value],
|
||||
gcoord[targetCrs.value]
|
||||
)
|
||||
|
||||
result.value = {
|
||||
type: 'crs',
|
||||
lon: transformed[0],
|
||||
lat: transformed[1]
|
||||
}
|
||||
}
|
||||
|
||||
const copyResult = () => {
|
||||
const text = result.value.type === 'decimal'
|
||||
? `纬度: ${result.value.lat}°, 经度: ${result.value.lon}°`
|
||||
: `纬度: ${result.value.lat}, 经度: ${result.value.lon}`
|
||||
let text = ''
|
||||
if (result.value.type === 'decimal') {
|
||||
text = `纬度: ${result.value.lat}, 经度: ${result.value.lon}`
|
||||
} else if (result.value.type === 'dms') {
|
||||
text = `纬度: ${result.value.lat}, 经度: ${result.value.lon}`
|
||||
} else {
|
||||
text = `${result.value.lon.toFixed(8)}, ${result.value.lat.toFixed(8)}`
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
alert('已复制到剪贴板!')
|
||||
@@ -135,7 +194,9 @@ const copyResult = () => {
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
margin-bottom: var(--spacing-md);
|
||||
@@ -148,25 +209,30 @@ const copyResult = () => {
|
||||
|
||||
.dms-input {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 0.6fr;
|
||||
gap: var(--spacing-sm);
|
||||
grid-template-columns: 1fr 1fr 1fr 0.8fr;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
margin-top: var(--spacing-xs);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-tertiary);
|
||||
.icon-sm { width: 16px; height: 16px; }
|
||||
.icon-md { width: 20px; height: 20px; }
|
||||
.icon-lg { width: 24px; height: 24px; vertical-align: bottom; }
|
||||
|
||||
.result-box {
|
||||
background: var(--bg-secondary);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--border-radius-sm);
|
||||
border-left: 4px solid var(--primary);
|
||||
}
|
||||
|
||||
.result-content {
|
||||
font-size: var(--font-size-lg);
|
||||
line-height: 1.8;
|
||||
.result-box p {
|
||||
margin: var(--spacing-xs) 0;
|
||||
font-family: monospace;
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.result-content p {
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
.btn-full { width: 100%; }
|
||||
|
||||
.text-success { color: #10b981; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dms-input {
|
||||
|
||||
Reference in New Issue
Block a user