feat: 引入面积计算器和坐标转换器组件,并添加新的依赖项和实用函数。

This commit is contained in:
2026-03-18 23:01:58 +08:00
parent 8e21bd9562
commit 8bf55b6716
6 changed files with 892 additions and 123 deletions

View File

@@ -13,16 +13,45 @@
<div class="grid grid-2 gap-xl">
<!-- Points Input -->
<div class="card">
<h3 class="card-title flex justify-between">
<span><LayoutList class="icon-md" /> 顶点坐标 (经度, 纬度)</span>
<button @click="addPoint" class="btn btn-secondary btn-sm"> 添加顶点</button>
</h3>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3 class="card-title" style="margin: 0; display: flex; align-items: center; gap: 8px;">
<LayoutList class="icon-md" /> 顶点坐标 (经度, 纬度)
</h3>
<button @click="addPoint" class="btn btn-secondary btn-sm" style="display: flex; align-items: center; gap: 4px; padding: 4px 12px; font-size: 0.875rem;">
添加顶点
</button>
</div>
<div class="mt-lg mb-md grid grid-2 gap-sm">
<div class="input-group">
<label class="input-label">坐标类型</label>
<select v-model="coordType" class="input" @change="handleModeChange">
<option value="lnglat">经纬度 (Lng/Lat)</option>
<option value="planar">平面坐标 (X/Y)</option>
</select>
</div>
<div class="input-group">
<label class="input-label">计算方式</label>
<select v-model="calcMethod" class="input" :disabled="coordType === 'planar'">
<option value="geodetic">测地线 (严密球面)</option>
<option value="planar">简单平面 (直角公式)</option>
</select>
</div>
</div>
<div class="points-list">
<div v-for="(point, index) in points" :key="index" class="point-input">
<span class="point-index">{{ index + 1 }}</span>
<input type="number" v-model.number="point.lng" class="input" placeholder="经度 (Lon)" step="0.000001">
<input type="number" v-model.number="point.lat" class="input" placeholder="纬度 (Lat)" step="0.000001">
<template v-if="coordType === 'lnglat'">
<input type="number" v-model.number="point.lng" class="input" placeholder="经度 (Lon)" step="0.000001">
<input type="number" v-model.number="point.lat" class="input" placeholder="纬度 (Lat)" step="0.000001">
</template>
<template v-else>
<input type="number" v-model.number="point.x" class="input" placeholder="X坐标 (北移)">
<input type="number" v-model.number="point.y" class="input" placeholder="Y坐标 (东移)">
</template>
<button v-if="points.length > 3" @click="removePoint(index)" class="btn-remove" title="删除">
<Trash2 class="icon-sm" />
</button>
@@ -30,13 +59,6 @@
</div>
<div class="mt-lg">
<div class="input-group">
<label class="input-label">计算模式</label>
<select v-model="calcMode" class="input">
<option value="geodetic">球面计算 (WGS84 测地线)</option>
<option value="planar">平面计算 (简单几何)</option>
</select>
</div>
<button @click="calculate" class="btn btn-primary btn-full mt-md">开始计算</button>
</div>
</div>
@@ -87,44 +109,109 @@ import * as turf from '@turf/turf'
import { ChevronLeft, SquareStack, LayoutList, Trash2, Activity, Info } from 'lucide-vue-next'
const points = ref([
{ lng: 116.391, lat: 39.907 },
{ lng: 116.401, lat: 39.907 },
{ lng: 116.401, lat: 39.917 },
{ lng: 116.391, lat: 39.917 }
{ lng: 108.5597255, lat: 22.8589543, x: 36557444.57, y: 2529026.75 },
{ lng: 108.5607409, lat: 22.8589507, x: 36557548.78, y: 2529026.75 },
{ lng: 108.5607373, lat: 22.8580860, x: 36557548.78, y: 2528930.99 },
{ lng: 108.5597220, lat: 22.8580896, x: 36557444.57, y: 2528930.99 }
])
const calcMode = ref('geodetic')
const coordType = ref('lnglat')
const calcMethod = ref('geodetic')
const area = ref(null)
const perimeter = ref(0)
const handleModeChange = () => {
area.value = null
perimeter.value = 0
if (coordType.value === 'planar') {
calcMethod.value = 'planar'
} else {
calcMethod.value = 'geodetic'
}
}
const addPoint = () => {
const last = points.value[points.value.length - 1]
points.value.push({ lng: last.lng + 0.01, lat: last.lat })
points.value.push({
lng: last.lng !== undefined ? Number(last.lng) + 0.001 : 116.391,
lat: last.lat !== undefined ? Number(last.lat) : 39.907,
x: last.x !== undefined ? Number(last.x) + 100 : 4419500,
y: last.y !== undefined ? Number(last.y) : 39533900
})
}
const removePoint = (index) => {
points.value.splice(index, 1)
}
const calculate = () => {
// Turf needs the first and last point to be the same to close the polygon
const coords = points.value.map(p => [p.lng, p.lat])
coords.push(coords[0])
try {
const polygon = turf.polygon([coords])
const calculatePlanar = (pts) => {
let a = 0;
let p = 0;
const n = pts.length;
for (let i = 0; i < n; i++) {
const j = (i + 1) % n;
// Shoelace: sum(x_i * y_{i+1} - x_{i+1} * y_i)
a += pts[i].x * pts[j].y;
a -= pts[j].x * pts[i].y;
if (calcMode.value === 'geodetic') {
// Perimeter
const dx = pts[j].x - pts[i].x;
const dy = pts[j].y - pts[i].y;
p += Math.sqrt(dx*dx + dy*dy);
}
return { area: Math.abs(a / 2), perimeter: p };
}
const calculate = () => {
try {
// Ensure all inputs are numbers
const rawPoints = points.value.map(p => ({
lng: Number(p.lng || 0),
lat: Number(p.lat || 0),
x: Number(p.x || 0),
y: Number(p.y || 0)
}));
if (coordType.value === 'lnglat' && calcMethod.value === 'geodetic') {
const coords = rawPoints.map(p => [p.lng, p.lat])
coords.push(coords[0])
const polygon = turf.polygon([coords])
area.value = turf.area(polygon)
perimeter.value = turf.length(polygon, { units: 'meters' })
} else {
// Simple planar area as fallback/comparison
area.value = Math.abs(turf.area(polygon)) // Turf area is always geodetic, for true planar you'd need a custom formula
// Note: Turf.js doesn't have a dedicated "planar" area for user-defined units,
// but we can simulate it or just use geodetic as it's more accurate for surveying.
let pts = [];
if (coordType.value === 'planar') {
// *** 关键改进:使用相对坐标偏移,防止大数计算丢失精度 ***
const x0 = rawPoints[0].x;
const y0 = rawPoints[0].y;
pts = rawPoints.map(p => ({ x: p.x - x0, y: p.y - y0 }));
} else {
// Approximate LngLat to meters using Equirectangular projection at local latitude
const latAvg = rawPoints.reduce((acc, cur) => acc + cur.lat, 0) / rawPoints.length;
const radLat = latAvg * Math.PI / 180;
const kx = 111319.49 * Math.cos(radLat);
const ky = 111319.49;
// Use relative coordinates to avoid large number precision issues with degree calculation
const lng0 = rawPoints[0].lng;
const lat0 = rawPoints[0].lat;
pts = rawPoints.map(p => ({
x: (p.lng - lng0) * kx,
y: (p.lat - lat0) * ky
}));
}
if (pts.length < 3) {
alert('顶点数量不足')
return
}
const res = calculatePlanar(pts)
area.value = res.area
perimeter.value = res.perimeter
}
} catch (e) {
alert('多边形无效,请检查坐标点顺序(需按顺时针或逆时针排列且不能自相交)')
alert('计算失败: ' + e.message)
}
}