271 lines
8.3 KiB
HTML
271 lines
8.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>地图距离计算器</title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: #f5f5f5;
|
|
}
|
|
h1 { color: #333; text-align: center; }
|
|
.section {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
.section h2 { margin-top: 0; color: #666; font-size: 16px; }
|
|
.input-group { display: flex; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
|
|
input[type="text"], input[type="file"] {
|
|
padding: 10px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
}
|
|
input[type="text"] { flex: 1; min-width: 200px; }
|
|
button {
|
|
padding: 10px 20px;
|
|
background: #1890ff;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
button:hover { background: #40a9ff; }
|
|
button:disabled { background: #ccc; cursor: not-allowed; }
|
|
.status { padding: 10px; border-radius: 4px; margin-top: 10px; }
|
|
.status.success { background: #f6ffed; color: #52c41a; }
|
|
.status.error { background: #fff2f0; color: #ff4d4f; }
|
|
.status.info { background: #e6f7ff; color: #1890ff; }
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 10px;
|
|
}
|
|
th, td {
|
|
padding: 10px;
|
|
text-align: left;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
th { background: #fafafa; font-weight: 600; }
|
|
.distance { color: #1890ff; font-weight: 500; }
|
|
.failed { color: #ff4d4f; }
|
|
#exportBtn { display: none; margin-top: 20px; }
|
|
.progress { color: #666; font-size: 14px; margin-top: 10px; }
|
|
.help { font-size: 12px; color: #999; margin-top: 5px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>地图距离计算器</h1>
|
|
|
|
<!-- Key 设置 -->
|
|
<div class="section">
|
|
<h2>高德地图 Key</h2>
|
|
<div class="input-group">
|
|
<input type="text" id="amapKey" placeholder="请输入高德地图 API Key">
|
|
<button onclick="saveKey()">保存 Key</button>
|
|
</div>
|
|
<div class="help">Key 将保存到浏览器本地存储</div>
|
|
<div id="keyStatus"></div>
|
|
</div>
|
|
|
|
<!-- 文件上传 -->
|
|
<div class="section">
|
|
<h2>上传 JSON 数据</h2>
|
|
<div class="input-group">
|
|
<input type="file" id="jsonFile" accept=".json">
|
|
<button onclick="uploadAndCalculate()">开始计算</button>
|
|
</div>
|
|
<div class="help">JSON 格式: [{"from": "北京市", "to": "上海市"}, ...]</div>
|
|
<div id="fileStatus"></div>
|
|
<div id="progress" class="progress"></div>
|
|
</div>
|
|
|
|
<!-- 结果表格 -->
|
|
<div class="section" id="resultSection" style="display: none;">
|
|
<h2>计算结果</h2>
|
|
<table id="resultTable">
|
|
<thead>
|
|
<tr>
|
|
<th>来源地址</th>
|
|
<th>目的地地址</th>
|
|
<th>距离 (km)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="resultBody"></tbody>
|
|
</table>
|
|
<button id="exportBtn" onclick="exportCSV()">导出 CSV</button>
|
|
</div>
|
|
|
|
<script>
|
|
const STORAGE_KEY = 'amap_key';
|
|
let results = [];
|
|
|
|
// 页面加载时读取 Key
|
|
window.onload = function() {
|
|
const savedKey = localStorage.getItem(STORAGE_KEY);
|
|
if (savedKey) {
|
|
document.getElementById('amapKey').value = savedKey;
|
|
showStatus('keyStatus', '已自动加载保存的 Key', 'success');
|
|
}
|
|
};
|
|
|
|
// 保存 Key
|
|
function saveKey() {
|
|
const key = document.getElementById('amapKey').value.trim();
|
|
if (!key) {
|
|
showStatus('keyStatus', '请输入 API Key', 'error');
|
|
return;
|
|
}
|
|
localStorage.setItem(STORAGE_KEY, key);
|
|
showStatus('keyStatus', 'Key 已保存到浏览器', 'success');
|
|
}
|
|
|
|
// 显示状态消息
|
|
function showStatus(elementId, message, type) {
|
|
const el = document.getElementById(elementId);
|
|
el.className = 'status ' + type;
|
|
el.textContent = message;
|
|
}
|
|
|
|
// 上传并计算
|
|
async function uploadAndCalculate() {
|
|
const key = localStorage.getItem(STORAGE_KEY);
|
|
if (!key) {
|
|
showStatus('fileStatus', '请先保存高德地图 API Key', 'error');
|
|
return;
|
|
}
|
|
|
|
const fileInput = document.getElementById('jsonFile');
|
|
if (!fileInput.files.length) {
|
|
showStatus('fileStatus', '请选择 JSON 文件', 'error');
|
|
return;
|
|
}
|
|
|
|
const file = fileInput.files[0];
|
|
showStatus('fileStatus', '正在读取文件...', 'info');
|
|
|
|
try {
|
|
const text = await file.text();
|
|
const data = JSON.parse(text);
|
|
|
|
if (!Array.isArray(data)) {
|
|
throw new Error('JSON 格式错误,应为数组格式');
|
|
}
|
|
|
|
showStatus('fileStatus', `开始计算 ${data.length} 条记录...`, 'info');
|
|
|
|
results = [];
|
|
const tbody = document.getElementById('resultBody');
|
|
tbody.innerHTML = '';
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
const item = data[i];
|
|
const from = item.from;
|
|
const to = item.to;
|
|
|
|
document.getElementById('progress').textContent =
|
|
`正在计算 ${i + 1}/${data.length}: ${from} → ${to}`;
|
|
|
|
const distance = await getDistance(from, to, key);
|
|
const result = { from, to, distance };
|
|
results.push(result);
|
|
|
|
// 添加表格行
|
|
const row = tbody.insertRow();
|
|
row.insertCell(0).textContent = from;
|
|
row.insertCell(1).textContent = to;
|
|
const distCell = row.insertCell(2);
|
|
distCell.textContent = distance;
|
|
distCell.className = distance.includes('失败') || distance.includes('解析失败') ? 'failed' : 'distance';
|
|
}
|
|
|
|
document.getElementById('progress').textContent = `计算完成,共 ${results.length} 条记录`;
|
|
document.getElementById('resultSection').style.display = 'block';
|
|
document.getElementById('exportBtn').style.display = 'inline-block';
|
|
showStatus('fileStatus', '计算完成', 'success');
|
|
|
|
} catch (err) {
|
|
showStatus('fileStatus', '错误: ' + err.message, 'error');
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 地理编码:将地址转换为经纬度
|
|
* 参考 index.ts 的 geocode 函数
|
|
*/
|
|
async function geocode(address, key) {
|
|
const url = `https://restapi.amap.com/v3/geocode/geo?address=${encodeURIComponent(address)}&key=${key}`;
|
|
const res = await fetch(url).then(r => r.json());
|
|
|
|
if (res.status === '1' && res.geocodes?.length > 0) {
|
|
const [lng, lat] = res.geocodes[0].location.split(',').map(Number);
|
|
return { lng, lat };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 计算两点间的直线距离(米)
|
|
* 参考 index.ts 的 calculateDistance 函数 (Haversine 公式)
|
|
*/
|
|
function calculateDistance(from, to) {
|
|
const R = 6371000; // 地球半径(米)
|
|
const dLat = (to.lat - from.lat) * Math.PI / 180;
|
|
const dLng = (to.lng - from.lng) * Math.PI / 180;
|
|
const a =
|
|
Math.sin(dLat / 2) ** 2 +
|
|
Math.cos(from.lat * Math.PI / 180) *
|
|
Math.cos(to.lat * Math.PI / 180) *
|
|
Math.sin(dLng / 2) ** 2;
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
return R * c;
|
|
}
|
|
|
|
/**
|
|
* 计算两个地址间的直线距离
|
|
* 参考 index.ts 的 getDistance 函数
|
|
*/
|
|
async function getDistance(fromAddress, toAddress, key) {
|
|
const [from, to] = await Promise.all([
|
|
geocode(fromAddress, key),
|
|
geocode(toAddress, key)
|
|
]);
|
|
|
|
if (!from || !to) {
|
|
return '解析失败';
|
|
}
|
|
|
|
const distance = calculateDistance(from, to);
|
|
return (distance / 1000).toFixed(2) + ' km';
|
|
}
|
|
|
|
// 导出 CSV
|
|
function exportCSV() {
|
|
if (!results.length) return;
|
|
|
|
const headers = ['from,to,distance\n'];
|
|
const rows = results.map(r =>
|
|
`"${r.from}","${r.to}","${r.distance}"`
|
|
);
|
|
const csv = headers.concat(rows).join('\n');
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'distance_results.csv';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|