add v0.0.2

This commit is contained in:
2026-01-09 23:14:45 +08:00
parent eb9f44b100
commit 941c321639
4 changed files with 451 additions and 3 deletions

107
plan/v0.0.2.md Normal file
View File

@@ -0,0 +1,107 @@
# 地图距离计算网页版 (v0.0.2)
## 需求
创建一个纯网页版的距离计算工具:
- AMAP_KEY 可编辑并保存到浏览器 localStorage
- 上传 JSON 数据文件计算多组距离
- 调用高德地图 API 计算距离
- 表格展示结果:来源地址 | 目的地地址 | 距离(km)
- 导出 CSV 功能
## JSON 输入格式
```json
[
{"from": "北京市", "to": "上海市"},
{"from": "广州市", "to": "深圳市"}
]
```
## 实现方案
1. **Key 管理**:使用 localStorage 保存/读取 AMAP_KEY
2. **文件上传**`<input type="file" accept=".json">` + FileReader 解析
3. **高德 API**:使用 `Geocode` 地理编码 API
- URL: `https://restapi.amap.com/v3/geocode/geo?address={address}&key={key}`
- 将地址转换为经纬度坐标
4. **距离计算**:使用 Haversine 公式计算两点间直线距离
- 地球半径: 6371000 米
- 转换为公里保留2位小数
5. **CSV 导出**:原生 JS 生成 Blob 下载
## 依赖
- **无外部 JS 库**,纯原生 HTML/CSS/JavaScript 实现
## 文件结构
```
public/
web.html # 完整的网页实现
```
## 关键代码
### 地理编码
```javascript
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;
}
```
### Haversine 距离计算
```javascript
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;
}
```
### 获取距离
```javascript
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 导出
```javascript
function exportCSV(results) {
const csv = 'from,to,distance\n' + results.map(r => `${r.from},${r.to},${r.distance}`).join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'distance_results.csv';
a.click();
}
```
## 验证步骤
1. 浏览器打开 `public/web.html`
2. 输入 AMAP_KEY 并保存
3. 上传测试 JSON 文件
4. 查看计算结果表格
5. 点击导出 CSV 验证下载

48
public/docs-web.md Normal file
View File

@@ -0,0 +1,48 @@
# 网页版地图距离计算器
在线使用:[https://kevisual.xiongxiao.me/root/test-map-distance/web.html](https://kevisual.xiongxiao.me/root/test-map-distance/web.html)
## 功能特性
- AMAP_KEY 保存到浏览器localStorage无需重复输入
- 上传 JSON 文件批量计算距离
- 使用 Haversine 公式计算两点间直线距离
- 结果表格展示
- 导出 CSV 文件
## 使用方法
### 1. 配置高德地图 Key
1. 访问 [高德开放平台](https://console.amap.com/dev/key/app) 注册/登录账号
2. 创建 Web 应用,获取 API Key
3. 在网页中输入 Key点击「保存 Key」
### 2. 准备 JSON 数据
创建 JSON 文件,格式如下:
```json
[
{"from": "北京市", "to": "上海市"},
{"from": "广州市", "to": "深圳市"},
{"from": "杭州市", "to": "南京市"}
]
```
### 3. 上传并计算
1. 点击「选择文件」,上传准备好的 JSON 文件
2. 点击「开始计算」
3. 等待计算完成,查看结果表格
### 4. 导出结果
计算完成后,点击「导出 CSV」下载结果文件。
## 注意事项
- 需要有效的 AMAP_KEY 才能正常使用
- 地理编码服务有每日调用限制
- 地址越精确,计算结果越准确
- 直线距离与实际行驶距离可能存在差异

View File

@@ -132,6 +132,13 @@
<div class="card">
<div class="card-header">
<h1>地图距离计算工具 - 使用说明</h1>
<!-- 文档切换导航 -->
<div class="mt-3">
<div class="btn-group" role="group">
<button type="button" class="btn btn-light btn-sm" id="btn-docs" onclick="loadDoc('docs.md', this)">脚本执行</button>
<button type="button" class="btn btn-light btn-sm active" id="btn-web-docs" onclick="loadDoc('docs-web.md', this)">网页版</button>
</div>
</div>
</div>
<div class="card-body" id="content">
<div class="loading">
@@ -154,9 +161,24 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>
async function loadDocs() {
// 文档切换功能
async function loadDoc(docFile, btn) {
// 更新按钮状态
document.querySelectorAll('.btn-group .btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// 显示加载状态
document.getElementById('content').innerHTML = `
<div class="loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="mt-3">文档加载中...</p>
</div>
`;
try {
const response = await fetch('./docs.md');
const response = await fetch('./' + docFile);
if (!response.ok) {
throw new Error('文档加载失败');
}
@@ -177,7 +199,8 @@
}
}
loadDocs();
// 默认加载网页版文档
loadDoc('docs-web.md', document.getElementById('btn-web-docs'));
</script>
</body>
</html>

270
public/web.html Normal file
View File

@@ -0,0 +1,270 @@
<!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>