feat: 引入面积计算器和坐标转换器组件,并添加新的依赖项和实用函数。
This commit is contained in:
@@ -1,94 +1,215 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="container pb-2xl">
|
||||
<div class="tool-header">
|
||||
<router-link to="/" class="back-link">
|
||||
<ChevronLeft class="icon-sm" /> 返回首页
|
||||
</router-link>
|
||||
<h2>
|
||||
<Globe class="icon-lg" /> 坐标转换工具
|
||||
<Globe class="icon-lg" /> 坐标系统与格式转换
|
||||
</h2>
|
||||
<p>支持经纬度格式转换及常用地图坐标系互转 (WGS84, GCJ02, BD09)</p>
|
||||
<p>支持经纬度格式转换、常用地图坐标系互转及 CGCS2000 高斯平面投影与批量转换</p>
|
||||
</div>
|
||||
|
||||
<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="tabs-wrapper">
|
||||
<!-- Tab Navigation -->
|
||||
<div class="tabs-header">
|
||||
<button
|
||||
v-for="tab in tabsList"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id"
|
||||
:class="['tab-btn', { active: activeTab === tab.id }]"
|
||||
>
|
||||
<component :is="tab.icon" class="icon-sm mr-xs" />
|
||||
{{ tab.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2 gap-md">
|
||||
<div>
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content card">
|
||||
|
||||
<!-- 1. Format Conversion -->
|
||||
<div v-show="activeTab === 'format'">
|
||||
<h3 class="card-title text-center mb-xl">度分秒与十进制度数互转</h3>
|
||||
<div class="grid grid-2 gap-xl">
|
||||
<div class="flex-col">
|
||||
<div class="input-group flex-1">
|
||||
<label class="input-label">度分秒 → 十进制</label>
|
||||
<div class="dms-input mb-sm">
|
||||
<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 px-xs">
|
||||
<option value="N">N</option>
|
||||
<option value="S">S</option>
|
||||
</select>
|
||||
</div>
|
||||
<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 px-xs">
|
||||
<option value="E">E</option>
|
||||
<option value="W">W</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="convertToDecimal" class="btn btn-primary btn-full mt-md">转换为十进制</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-col">
|
||||
<div class="input-group flex-1">
|
||||
<label class="input-label">十进制 → 度分秒</label>
|
||||
<div class="flex flex-col gap-sm">
|
||||
<input type="number" v-model.number="decimal.lat" class="input" placeholder="纬度 (Lat)">
|
||||
<input type="number" v-model.number="decimal.lon" class="input" placeholder="经度 (Lon)">
|
||||
</div>
|
||||
</div>
|
||||
<button @click="convertToDms" class="btn btn-primary btn-full mt-md">转换为度分秒</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. CRS Transformation -->
|
||||
<div v-show="activeTab === 'crs'">
|
||||
<h3 class="card-title text-center mb-xl">常用地图坐标系互转</h3>
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<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>
|
||||
<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 mt-md">
|
||||
<div class="input-group">
|
||||
<label class="input-label">来源坐标系</label>
|
||||
<select v-model="sourceCrs" class="input">
|
||||
<option value="WGS84">WGS84 (大地坐标/GPS)</option>
|
||||
<option value="GCJ02">GCJ02 (火星坐标/高德/腾讯)</option>
|
||||
<option value="BD09">BD09 (百度坐标)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label class="input-label">目标坐标系</label>
|
||||
<select v-model="targetCrs" class="input">
|
||||
<option value="GCJ02">GCJ02 (火星坐标/高德/腾讯)</option>
|
||||
<option value="WGS84">WGS84 (大地坐标/GPS)</option>
|
||||
<option value="BD09">BD09 (百度坐标)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="convertToDecimal" class="btn btn-primary btn-full mt-md">转换为十进制</button>
|
||||
</div>
|
||||
|
||||
<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="经度">
|
||||
<button @click="transformCrs" class="btn btn-primary btn-full mt-lg">执行地图坐标转换</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. CGCS2000 Projection -->
|
||||
<div v-show="activeTab === 'proj'">
|
||||
<h3 class="card-title text-center mb-xl">高斯-克吕格平面投影计算</h3>
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="grid grid-2 gap-md mb-md">
|
||||
<div class="input-group">
|
||||
<label class="input-label">转换方向</label>
|
||||
<select v-model="projConfig.direction" class="input">
|
||||
<option value="toPlane">经纬度 → 平面坐标</option>
|
||||
<option value="toLngLat">平面坐标 → 经纬度</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label class="input-label">分带方式 (CGCS2000)</label>
|
||||
<select v-model="projConfig.zoneType" class="input">
|
||||
<option :value="3">3 度带</option>
|
||||
<option :value="6">6 度带</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="convertToDms" class="btn btn-primary btn-full mt-md">转换为度分秒</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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 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 v-if="projConfig.direction === 'toPlane'" class="flex-col pb-md">
|
||||
<div class="grid grid-2 gap-md mb-md">
|
||||
<div class="input-group">
|
||||
<label class="input-label">经度 (Lon)</label>
|
||||
<input type="number" v-model.number="projConfig.lng" class="input" placeholder="输入经度">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label class="input-label">纬度 (Lat)</label>
|
||||
<input type="number" v-model.number="projConfig.lat" class="input" placeholder="输入纬度">
|
||||
</div>
|
||||
</div>
|
||||
<label class="checkbox-label mb-md">
|
||||
<input type="checkbox" v-model="projConfig.withZone">
|
||||
投影后的 X 坐标 (北移/纬距对应国内Y) 包含带号
|
||||
</label>
|
||||
<button @click="doProjection" class="btn btn-primary btn-full">投影到平面</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex-col pb-md">
|
||||
<div class="grid grid-2 gap-md mb-md">
|
||||
<div class="input-group">
|
||||
<label class="input-label">北移坐标 (国内X)</label>
|
||||
<input type="number" v-model.number="projConfig.y" class="input" placeholder="对应测量的 X 坐标 (Northing)">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label class="input-label">东移坐标 (国内Y)</label>
|
||||
<input type="number" v-model.number="projConfig.x" class="input" placeholder="对应测量的 Y 坐标 (Easting)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2 gap-md mb-md align-center">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="projConfig.withZone"> 东移坐标包含带号
|
||||
</label>
|
||||
<div class="input-group m-0" v-if="!projConfig.withZone">
|
||||
<input type="number" v-model.number="projConfig.manualZone" class="input" placeholder="请手动输入区域带号">
|
||||
</div>
|
||||
</div>
|
||||
<button @click="doProjection" class="btn btn-primary btn-full">反投到经纬度</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. Execute Bulk Conversion -->
|
||||
<div v-show="activeTab === 'batch'">
|
||||
<h3 class="card-title text-center mb-md">通过 Excel 批量互转平面与地理坐标</h3>
|
||||
<p class="text-secondary text-sm text-center mb-xl">
|
||||
请先下载对应的格式模板,填写数据后上传即可自动解析、计算并导出。所有计算均在浏览器本地完成。
|
||||
</p>
|
||||
|
||||
<div class="max-w-3xl mx-auto flex-col">
|
||||
<div class="grid grid-2 gap-md mb-xl">
|
||||
<button @click="downloadTemplate('toPlane')" class="btn btn-secondary flex-center gap-xs">
|
||||
<DownloadCloud class="icon-sm" /> 下载 经纬度转平面 模板
|
||||
</button>
|
||||
<button @click="downloadTemplate('toLngLat')" class="btn btn-secondary flex-center gap-xs">
|
||||
<DownloadCloud class="icon-sm" /> 下载 平面转经纬度 模板
|
||||
</button>
|
||||
<button @click="downloadTemplate('toDecimal')" class="btn btn-secondary flex-center gap-xs">
|
||||
<DownloadCloud class="icon-sm" /> 下载 度分秒转十进制 模板
|
||||
</button>
|
||||
<button @click="downloadTemplate('toDms')" class="btn btn-secondary flex-center gap-xs">
|
||||
<DownloadCloud class="icon-sm" /> 下载 十进制转度分秒 模板
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="upload-area">
|
||||
<input type="file" id="excel-upload" accept=".xlsx, .xls" class="hidden" @change="handleFileUpload">
|
||||
<label for="excel-upload" class="upload-label">
|
||||
<UploadCloud class="icon-xl mb-md text-primary" />
|
||||
<span class="text-lg font-600 mb-xs">点击此处选择 Excel 文件</span>
|
||||
<span class="text-tertiary text-sm">支持 .xlsx, .xls 格式</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="transformCrs" class="btn btn-primary btn-full mt-md">执行转换</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Area -->
|
||||
<div v-if="result" class="card mt-xl">
|
||||
<!-- Single Result Area -->
|
||||
<div v-if="result && activeTab !== 'batch'" 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" /> 转换结果
|
||||
<CheckCircle2 class="icon-md text-success" /> 单点计算结果
|
||||
</h4>
|
||||
<button @click="copyResult" class="btn btn-secondary btn-sm">
|
||||
<Copy class="icon-sm" /> 复制结果
|
||||
<button @click="copyResult" class="btn btn-secondary btn-sm flex-center gap-xs">
|
||||
<Copy class="icon-sm" /> 复制
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -102,10 +223,20 @@
|
||||
<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>
|
||||
<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 v-else-if="result.type === 'proj_plane'">
|
||||
<p><strong>北移坐标 (国内X):</strong> {{ result.northing.toFixed(4) }} m</p>
|
||||
<p><strong>东移坐标 (国内Y):</strong> {{ result.easting.toFixed(4) }} m</p>
|
||||
<small class="text-tertiary">基于 CGCS2000 {{ projConfig.zoneType }}度带高斯投影 (带号: {{ result.zone }}, 中央子午线: {{ result.meridian }}°)</small>
|
||||
</div>
|
||||
<div v-else-if="result.type === 'proj_lnglat'">
|
||||
<p><strong>经度 (Lon):</strong> {{ result.lng.toFixed(8) }}°</p>
|
||||
<p><strong>纬度 (Lat):</strong> {{ result.lat.toFixed(8) }}°</p>
|
||||
<small class="text-tertiary">基于 CGCS2000 {{ projConfig.zoneType }}度带高斯投影 (带号: {{ result.zone }}, 中央子午线: {{ result.meridian }}°)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,31 +245,56 @@
|
||||
<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'
|
||||
import * as xlsx from 'xlsx'
|
||||
import { ChevronLeft, Globe, RefreshCw, MapPin, Copy, CheckCircle2, Layers, FileSpreadsheet, DownloadCloud, UploadCloud } from 'lucide-vue-next'
|
||||
import { dmsToDecimal, formatCoordinate } from '../utils/coordinate'
|
||||
import { projectToPlane, unprojectToLngLat } from '../utils/proj'
|
||||
|
||||
// DMS Conversion State
|
||||
// === Tabs UI State ===
|
||||
const activeTab = ref('format')
|
||||
const tabsList = [
|
||||
{ id: 'format', name: '格式转换', icon: RefreshCw },
|
||||
{ id: 'crs', name: '地图坐标转换', icon: MapPin },
|
||||
{ id: 'proj', name: 'CGCS2000投影转换', icon: Layers },
|
||||
{ id: 'batch', name: 'Excel 批量互转', icon: FileSpreadsheet }
|
||||
]
|
||||
|
||||
// === DMS Conversion State ===
|
||||
const dms = ref({
|
||||
lat: { degrees: 39, minutes: 54, seconds: 27, direction: 'N' },
|
||||
lon: { degrees: 116, minutes: 23, seconds: 50, direction: 'E' }
|
||||
lat: { degrees: 22, minutes: 49, seconds: 16.8640, direction: 'N' },
|
||||
lon: { degrees: 108, minutes: 18, seconds: 37.4184, direction: 'E' }
|
||||
})
|
||||
const decimal = ref({ lat: 39.9075, lon: 116.3972 })
|
||||
const decimal = ref({ lat: 22.8213511, lon: 108.3103940 })
|
||||
|
||||
// CRS Transformation State
|
||||
// === CRS Transformation State ===
|
||||
const crsInput = ref('116.397451, 39.909235')
|
||||
const sourceCrs = ref('WGS84')
|
||||
const targetCrs = ref('GCJ02')
|
||||
|
||||
// === Projection State ===
|
||||
const projConfig = ref({
|
||||
direction: 'toPlane', // or 'toLngLat'
|
||||
zoneType: 3, // 3 or 6
|
||||
withZone: true,
|
||||
lng: 116.397451,
|
||||
lat: 39.909235,
|
||||
x: 39533966.82, // Easting (Y)
|
||||
y: 4419519.82, // Northing (X)
|
||||
manualZone: 39
|
||||
})
|
||||
|
||||
const result = ref(null)
|
||||
|
||||
// --- Methods ---
|
||||
|
||||
const convertToDecimal = () => {
|
||||
const latDecimal = dmsToDecimal(dms.value.lat.degrees, dms.value.lat.minutes, dms.value.lat.seconds)
|
||||
const lonDecimal = dmsToDecimal(dms.value.lon.degrees, dms.value.lon.minutes, dms.value.lon.seconds)
|
||||
|
||||
result.value = {
|
||||
type: 'decimal',
|
||||
lat: (dms.value.lat.direction === 'S' ? -latDecimal : latDecimal).toFixed(6),
|
||||
lon: (dms.value.lon.direction === 'W' ? -lonDecimal : lonDecimal).toFixed(6)
|
||||
lat: (dms.value.lat.direction === 'S' ? -latDecimal : latDecimal).toFixed(8),
|
||||
lon: (dms.value.lon.direction === 'W' ? -lonDecimal : lonDecimal).toFixed(8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,23 +327,220 @@ const transformCrs = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const doProjection = () => {
|
||||
try {
|
||||
if (projConfig.value.direction === 'toPlane') {
|
||||
const { lng, lat, zoneType, withZone } = projConfig.value
|
||||
if (!lng || !lat) return alert('请输入经纬度')
|
||||
const res = projectToPlane(lng, lat, zoneType, withZone)
|
||||
result.value = {
|
||||
type: 'proj_plane',
|
||||
easting: res.easting,
|
||||
northing: res.northing,
|
||||
zone: res.zone,
|
||||
meridian: res.meridian
|
||||
}
|
||||
} else {
|
||||
const { x, y, zoneType, withZone, manualZone } = projConfig.value
|
||||
if (!x || !y) return alert('请输入平面坐标')
|
||||
const res = unprojectToLngLat(x, y, zoneType, withZone, manualZone)
|
||||
result.value = {
|
||||
type: 'proj_lnglat',
|
||||
lng: res.lng,
|
||||
lat: res.lat,
|
||||
zone: res.zone,
|
||||
meridian: res.meridian
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
alert('转换失败: ' + err.message)
|
||||
}
|
||||
}
|
||||
|
||||
const copyResult = () => {
|
||||
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 {
|
||||
} else if (result.value.type === 'crs') {
|
||||
text = `${result.value.lon.toFixed(8)}, ${result.value.lat.toFixed(8)}`
|
||||
} else if (result.value.type === 'proj_plane') {
|
||||
text = `X (北移): ${result.value.northing.toFixed(4)}, Y (东移): ${result.value.easting.toFixed(4)}`
|
||||
} else {
|
||||
text = `${result.value.lng.toFixed(8)}, ${result.value.lat.toFixed(8)}`
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
alert('已复制到剪贴板!')
|
||||
})
|
||||
}
|
||||
|
||||
// === Bulk Excel Conversion ===
|
||||
|
||||
const downloadTemplate = (type) => {
|
||||
let wsData = []
|
||||
let filename = ''
|
||||
if (type === 'toPlane') {
|
||||
wsData = [['点名', '经度', '纬度', '分带类型(3/6)', 'X包含带号(1/0)'], ['pt1', 116.397451, 39.909235, 3, 1]]
|
||||
filename = '经纬度转平面模板.xlsx'
|
||||
} else if (type === 'toLngLat') {
|
||||
wsData = [['点名', 'X坐标(北移)', 'Y坐标(东移)', '分带类型(3/6)', '手动带号'], ['pt1', 4419519.82, 39533966.82, 3, '']]
|
||||
filename = '平面转经纬度模板.xlsx'
|
||||
} else if (type === 'toDecimal') {
|
||||
wsData = [['点名', '纬度度', '纬度分', '纬度秒', '纬度方向(N/S)', '经度度', '经度分', '经度秒', '经度方向(E/W)'], ['pt1', 39, 54, 27, 'N', 116, 23, 50, 'E']]
|
||||
filename = '度分秒转十进制模板.xlsx'
|
||||
} else if (type === 'toDms') {
|
||||
wsData = [['点名', '纬度(十进制)', '经度(十进制)'], ['pt1', 39.9075, 116.3972]]
|
||||
filename = '十进制转度分秒模板.xlsx'
|
||||
}
|
||||
|
||||
const ws = xlsx.utils.aoa_to_sheet(wsData)
|
||||
const wb = xlsx.utils.book_new()
|
||||
xlsx.utils.book_append_sheet(wb, ws, "Sheet1")
|
||||
xlsx.writeFile(wb, filename)
|
||||
}
|
||||
|
||||
const handleFileUpload = (e) => {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (evt) => {
|
||||
try {
|
||||
const data = evt.target.result
|
||||
const wb = xlsx.read(data, { type: 'binary' })
|
||||
const wsName = wb.SheetNames[0]
|
||||
const ws = wb.Sheets[wsName]
|
||||
const json = xlsx.utils.sheet_to_json(ws)
|
||||
if (json.length === 0) return alert('Excel无数据')
|
||||
|
||||
// 识别是哪种模式 (通过列名)
|
||||
const first = json[0]
|
||||
const isToDecimal = '纬度度' in first
|
||||
const isToDms = '纬度(十进制)' in first || '经度(十进制)' in first
|
||||
const isToPlane = !isToDecimal && !isToDms && ('经度' in first || '经度(Lon)' in first)
|
||||
const isToLngLat = 'X坐标(北移)' in first || 'X坐标' in first
|
||||
|
||||
const resultData = []
|
||||
|
||||
for (let i = 0; i < json.length; i++) {
|
||||
const row = json[i]
|
||||
const name = row['点名'] || `Point${i+1}`
|
||||
const zoneType = parseInt(row['分带类型(3/6)'] || projConfig.value.zoneType)
|
||||
|
||||
if (isToDecimal) {
|
||||
const latDeg = parseFloat(row['纬度度']) || 0
|
||||
const latMin = parseFloat(row['纬度分']) || 0
|
||||
const latSec = parseFloat(row['纬度秒']) || 0
|
||||
const latDir = row['纬度方向(N/S)'] || 'N'
|
||||
|
||||
const lonDeg = parseFloat(row['经度度']) || 0
|
||||
const lonMin = parseFloat(row['经度分']) || 0
|
||||
const lonSec = parseFloat(row['经度秒']) || 0
|
||||
const lonDir = row['经度方向(E/W)'] || 'E'
|
||||
|
||||
try {
|
||||
let latDec = dmsToDecimal(latDeg, latMin, latSec)
|
||||
if (latDir.toUpperCase() === 'S') latDec = -latDec
|
||||
let lonDec = dmsToDecimal(lonDeg, lonMin, lonSec)
|
||||
if (lonDir.toUpperCase() === 'W') lonDec = -lonDec
|
||||
|
||||
resultData.push({
|
||||
...row,
|
||||
'点名': name,
|
||||
'纬度(十进制)': latDec.toFixed(8),
|
||||
'经度(十进制)': lonDec.toFixed(8)
|
||||
})
|
||||
} catch (err) {
|
||||
resultData.push({ ...row, '点名': name, '错误': err.message })
|
||||
}
|
||||
} else if (isToDms) {
|
||||
const latDec = parseFloat(row['纬度(十进制)'])
|
||||
const lonDec = parseFloat(row['经度(十进制)'])
|
||||
|
||||
if (!isNaN(latDec) && !isNaN(lonDec)) {
|
||||
try {
|
||||
resultData.push({
|
||||
...row,
|
||||
'点名': name,
|
||||
'纬度(度分秒)': formatCoordinate(latDec, 'lat'),
|
||||
'经度(度分秒)': formatCoordinate(lonDec, 'lon')
|
||||
})
|
||||
} catch (err) {
|
||||
resultData.push({ ...row, '点名': name, '错误': err.message })
|
||||
}
|
||||
}
|
||||
} else if (isToPlane) {
|
||||
const lng = parseFloat(row['经度']) || parseFloat(row['经度(Lon)'])
|
||||
const lat = parseFloat(row['纬度']) || parseFloat(row['纬度(Lat)'])
|
||||
const withZone = (row['X包含带号(1/0)'] !== undefined) ? (parseInt(row['X包含带号(1/0)']) === 1) : true
|
||||
|
||||
if (lng && lat) {
|
||||
try {
|
||||
const res = projectToPlane(lng, lat, zoneType, withZone)
|
||||
resultData.push({
|
||||
...row,
|
||||
'点名': name,
|
||||
'经度': lng,
|
||||
'纬度': lat,
|
||||
'X坐标(北移/Northing)': res.northing.toFixed(4),
|
||||
'Y坐标(东移/Easting)': res.easting.toFixed(4),
|
||||
'带号': res.zone,
|
||||
'中央子午线': res.meridian
|
||||
})
|
||||
} catch (err) {
|
||||
resultData.push({ ...row, '点名': name, '错误': err.message })
|
||||
}
|
||||
}
|
||||
} else if (isToLngLat) {
|
||||
const northing = parseFloat(row['X坐标(北移)']) || parseFloat(row['X坐标'])
|
||||
const easting = parseFloat(row['Y坐标(东移)']) || parseFloat(row['Y坐标'])
|
||||
const manualZone = row['手动带号'] ? parseInt(row['手动带号']) : null
|
||||
|
||||
const withZone = easting > 1000000
|
||||
|
||||
if (northing && easting) {
|
||||
try {
|
||||
const res = unprojectToLngLat(easting, northing, zoneType, withZone, manualZone)
|
||||
resultData.push({
|
||||
...row,
|
||||
'点名': name,
|
||||
'X坐标(北)': northing,
|
||||
'Y坐标(东)': easting,
|
||||
'经度': res.lng.toFixed(8),
|
||||
'纬度': res.lat.toFixed(8),
|
||||
'带号': res.zone,
|
||||
'中央子午线': res.meridian
|
||||
})
|
||||
} catch (err) {
|
||||
resultData.push({ ...row, '点名': name, '错误': err.message })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newWs = xlsx.utils.json_to_sheet(resultData)
|
||||
const newWb = xlsx.utils.book_new()
|
||||
xlsx.utils.book_append_sheet(newWb, newWs, "转换结果")
|
||||
xlsx.writeFile(newWb, `坐标转换结果_${Date.now()}.xlsx`)
|
||||
|
||||
// clear input
|
||||
e.target.value = ''
|
||||
|
||||
} catch (err) {
|
||||
alert('文件解析失败: ' + err.message)
|
||||
}
|
||||
}
|
||||
reader.readAsBinaryString(file)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container.pb-2xl {
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.tool-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
@@ -207,15 +560,108 @@ const copyResult = () => {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* --- Tabs Styling --- */
|
||||
.tabs-wrapper {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.tabs-header {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
.tabs-header::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari */
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all var(--transition-base);
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: var(--primary);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
|
||||
.mr-xs {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.mb-lg {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
.max-w-3xl {
|
||||
max-width: 768px;
|
||||
}
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dms-input {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 0.8fr;
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.dms-input .input {
|
||||
width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.px-xs {
|
||||
padding-left: var(--spacing-xs);
|
||||
padding-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.icon-sm { width: 16px; height: 16px; }
|
||||
.icon-md { width: 20px; height: 20px; }
|
||||
.icon-lg { width: 24px; height: 24px; vertical-align: bottom; }
|
||||
.icon-xl { width: 48px; height: 48px; }
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
background: var(--bg-secondary);
|
||||
@@ -231,12 +677,33 @@ const copyResult = () => {
|
||||
}
|
||||
|
||||
.btn-full { width: 100%; }
|
||||
|
||||
.text-success { color: #10b981; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dms-input {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.upload-area {
|
||||
margin-top: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.upload-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.upload-label:hover {
|
||||
border-color: var(--primary);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user