一、引言:为什么选择 Python + 高德地图 + JavaScript?
在数字化时代,地图应用已渗透到生活的方方面面 —— 从外卖 APP 的骑手定位,到旅游网站的景点导航,再到企业的物流追踪系统。而实现 “后端数据处理 + 前端地图可视化” 的组合,Python(灵活的后端数据处理能力)、高德地图(成熟的地图服务 API)、JavaScript(前端交互核心)无疑是性价比极高的技术栈。
本文将以 “周边餐饮查询工具” 为实战案例,带大家完整实现:Python 后端调用高德地图 Web 服务 API 获取地理数据 → 搭建 API 接口供前端调用 → 用 JavaScript 结合高德地图 JS API 在网页显示地图并加载数据,全程代码可复现,关键步骤用表格拆解,即使是编程新手也能跟随操作。
二、基础准备:工具、账号与技术栈选型
在开始开发前,我们需要完成 “账号申请 + 环境搭建 + 技术选型” 三大准备工作,这是后续开发的基础。
2.1 必备工具与账号
类别 | 具体内容 | 用途说明 | 获取方式 |
开发工具 | VS Code | 代码编写(支持 Python/JS/HTML,插件丰富) | |
开发工具 | Postman | 测试 Python 后端 API 接口 | |
开发工具 | Anaconda | 管理 Python 虚拟环境(避免依赖冲突) | |
账号资源 | 高德地图开发者账号 | 获取调用 API 的 Key(核心凭证) | |
浏览器 | Chrome/Firefox | 调试前端网页地图 | 电脑自带或官网下载 |
2.2 技术栈选型对比
不同技术栈适用于不同场景,下表为你分析各组件的选型理由,帮助你理解 “为什么选这些工具”:
技术模块 | 可选方案 | 本文选型 | 选型理由 |
Python 后端框架 | Flask / Django / FastAPI | Flask | 轻量级、学习成本低,适合小型 API 开发;无需复杂配置,快速上手 |
HTTP 请求库 | requests / aiohttp | requests | 同步请求足够满足需求,API 简洁,文档丰富,新手友好 |
前端地图 API | 高德地图 JS API / 百度地图 JS API / 谷歌地图 JS API | 高德地图 JS API | 国内访问稳定,文档全中文,支持 POI 搜索、路径规划等功能,免费额度充足 |
前端数据交互 | 原生 JavaScript /jQuery/ Axios | 原生 JavaScript + Axios | 原生 JS 兼容性好,Axios 简化异步请求,避免 jQuery 的冗余代码 |
数据格式 | JSON / XML / CSV | JSON | 前后端数据交互的主流格式,Python 和 JS 都能轻松解析 |
2.3 高德地图 API Key 申请步骤(关键!)
调用高德地图的所有功能都需要 “API Key”,请严格按照以下步骤操作:
- 注册账号:访问高德开发者平台,用手机号注册并完成实名认证(个人开发者即可,免费额度足够测试)。
- 创建应用:
-
- 登录后进入「控制台」→「应用管理」→「创建应用」,输入应用名称(如 “Python 高德地图 Demo”),选择应用类型(如 “Web 前端”)。
- 申请 API Key:
-
- 在应用下点击「添加 Key」,选择 Key 类型(需区分 “Web 服务 API” 和 “JS API”,本文需同时申请两种):
-
-
- Web 服务 API Key:用于 Python 后端调用(如获取 POI 数据、地理编码),需勾选 “Web 服务” 权限。
-
-
-
- JS API Key:用于前端网页显示地图,需勾选 “JS API” 权限,并设置 “Referer 白名单”(开发阶段可填localhost或*,生产环境需填真实域名)。
-
Key 类型 | 用途 | 权限要求 | 白名单设置 |
Web 服务 API Key | Python 后端获取地理数据 | 勾选 “Web 服务” | 无需设置 |
JS API Key | JavaScript 前端显示地图 | 勾选 “JS API” | 开发:localhost;生产:真实域名(如www.example.com) |
三、Python 后端开发:搭建地理数据 API 服务
Python 后端的核心作用是:调用高德地图 Web 服务 API 获取地理数据(如 POI、经纬度),并封装成 HTTP 接口供前端调用。本节将分 “环境搭建→核心功能实现→API 接口封装” 三部分讲解。
3.1 环境搭建:安装依赖库
打开 Anaconda Prompt,创建并激活虚拟环境,然后安装所需库:
# 创建虚拟环境(名为amap-demo,Python版本3.9)
conda create -n amap-demo python=3.9
# 激活虚拟环境
conda activate amap-demo
# 安装依赖库
pip install flask requests flask-cors python-dotenv
各库的用途如下表所示:
库名称 | 用途 | 关键功能 |
Flask | 轻量级 Python Web 框架 | 搭建 HTTP 接口(如/api/poi)、处理前端请求 |
requests | 发送 HTTP 请求 | 调用高德地图 Web 服务 API(如 POI 搜索接口) |
flask-cors | 解决跨域问题 | 允许前端(如localhost:5500)访问后端(如localhost:5000) |
python-dotenv | 管理环境变量 | 存储 API Key 等敏感信息(避免硬编码到代码中) |
3.2 核心功能 1:调用高德地图 Web 服务 API 获取数据
首先,我们需要封装一个 Python 工具类,用于调用高德地图的 Web 服务 API(如 POI 搜索、地理编码)。创建amap_utils.py文件,代码如下:
import requests
from dotenv import load_dotenv
import os
# 加载环境变量(从.env文件读取API Key)
load_dotenv()
AMAP_WEB_KEY = os.getenv("AMAP_WEB_KEY") # 从.env文件获取Web服务API Key
class AMapWebAPI:
def __init__(self, api_key=AMAP_WEB_KEY):
self.api_key = api_key
# 高德地图Web服务API基础URL(参考官方文档)
self.base_url = "https://restapi.amap.com/v3"
def search_poi(self, keywords, city, page=1, page_size=20):
"""
搜索POI(兴趣点)数据(如周边餐厅、酒店)
:param keywords: 搜索关键词(如“餐厅”“咖啡馆”)
:param city: 城市名称(如“北京”“上海”)
:param page: 页码(默认第1页)
:param page_size: 每页结果数(默认20条,最大50条)
:return: 格式化后的POI数据(JSON)
"""
# POI搜索接口URL
url = f"{self.base_url}/place/text"
# 请求参数(参考高德Web服务API文档)
params = {
"key": self.api_key,
"keywords": keywords,
"city": city,
"page": page,
"offset": page_size,
"output": "json", # 返回格式:json/xml
"extensions": "all" # 返回详细信息(如地址、电话、经纬度)
}
try:
# 发送GET请求
response = requests.get(url, params=params)
response.raise_for_status() # 若状态码非200,抛出异常
data = response.json()
# 校验返回结果是否成功
if data["status"] == "1":
# 提取关键信息(过滤无用字段,方便前端使用)
result = {
"total": data["count"], # 总结果数
"page": page,
"page_size": page_size,
"pois": [
{
"id": poi["id"], # POI唯一ID
"name": poi["name"], # 名称
"address": poi["address"], # 地址
"location": poi["location"], # 经纬度(格式:lng,lat)
"tel": poi.get("tel", "无"), # 电话(若无则显示“无”)
"distance": poi.get("distance", 0) # 距离中心点的距离(米,若无则0)
}
for poi in data["pois"]
]
}
return {"success": True, "data": result}
else:
return {"success": False, "error": f"获取POI失败:{data['info']}"}
except Exception as e:
return {"success": False, "error": f"请求异常:{str(e)}"}
def geocode(self, address, city=None):
"""
地理编码(地址→经纬度)
:param address: 详细地址(如“北京市海淀区中关村大街1号”)
:param city: 城市(可选,缩小搜索范围,提高准确性)
:return: 经纬度数据(JSON)
"""
url = f"{self.base_url}/geocode/geo"
params = {
"key": self.api_key,
"address": address,
"city": city,
"output": "json"
}
try:
response = requests.get(url, params=params)
response.raise_for_status()
data = response.json()
if data["status"] == "1" and len(data["geocodes"]) > 0:
geocode_info = data["geocodes"][0]
result = {
"address": geocode_info["formatted_address"], # 格式化地址
"location": geocode_info["location"], # 经纬度(lng,lat)
"city": geocode_info["city"], # 城市名称
"adcode": geocode_info["adcode"] # 行政区划代码
}
return {"success": True, "data": result}
else:
return {"success": False, "error": f"地理编码失败:{data['info']}"}
except Exception as e:
return {"success": False, "error": f"请求异常:{str(e)}"}
def reverse_geocode(self, location):
"""
逆地理编码(经纬度→地址)
:param location: 经纬度(格式:lng,lat,如“116.481028,39.921983”)
:return: 地址数据(JSON)
"""
url = f"{self.base_url}/geocode/regeo"
params = {
"key": self.api_key,
"location": location,
"output": "json",
"extensions": "all" # 返回详细信息(如街道、门牌号)
}
try:
response = requests.get(url, params=params)
response.raise_for_status()
data = response.json()
if data["status"] == "1":
regeo_info = data["regeocode"]
result = {
"address": regeo_info["formatted_address"], # 格式化地址
"street": regeo_info["addressComponent"]["street"], # 街道
"number": regeo_info["addressComponent"]["streetNumber"], # 门牌号
"city": regeo_info["addressComponent"]["city"], # 城市
"district": regeo_info["addressComponent"]["district"] # 区县
}
return {"success": True, "data": result}
else:
return {"success": False, "error": f"逆地理编码失败:{data['info']}"}
except Exception as e:
return {"success": False, "error": f"请求异常:{str(e)}"}
关键说明(用表格拆解):
功能方法 | 输入参数 | 输出结果 | 核心逻辑 |
search_poi | keywords(关键词)、city(城市)、page(页码)、page_size(每页条数) | 包含总结果数、当前页 POI 列表(名称、地址、经纬度等)的 JSON | 1. 拼接 POI 搜索接口 URL;2. 传入 API Key 和搜索参数;3. 解析响应,过滤无用字段;4. 返回格式化数据 |
geocode | address(地址)、city(可选城市) | 包含格式化地址、经纬度、城市的 JSON | 1. 调用地理编码接口;2. 提取第一个匹配结果的经纬度;3. 返回结构化地址信息 |
reverse_geocode | location(经纬度) | 包含详细地址、街道、门牌号的 JSON | 1. 调用逆地理编码接口;2. 解析响应中的地址组件;3. 返回人类可读的详细地址 |
环境变量配置(.env 文件):
为了避免将 API Key 硬编码到代码中(不安全,且便于切换环境),创建.env文件,内容如下:
# .env文件(与amap_utils.py同级目录)
AMAP_WEB_KEY=你的高德地图Web服务API Key # 替换为你申请的Key
3.3 核心功能 2:用 Flask 封装 API 接口
接下来,创建app.py文件,用 Flask 搭建 HTTP 接口,供前端调用 Python 后端的地理数据功能。代码如下:
from flask import Flask, request, jsonify
from flask_cors import CORS
from amap_utils import AMapWebAPI
# 初始化Flask应用
app = Flask(__name__)
# 允许跨域(解决前端和后端域名/端口不同导致的请求被拦截问题)
CORS(app, resources=r"/*") # 开发阶段允许所有请求跨域,生产环境需限制域名
# 初始化高德地图Web API工具类
amap_api = AMapWebAPI()
# 1. POI搜索接口(前端调用此接口获取周边POI数据)
@app.route("/api/poi", methods=["GET"])
def get_poi():
# 从前端请求中获取参数(若参数不存在,用默认值)
keywords = request.args.get("keywords", "餐厅") # 默认搜索“餐厅”
city = request.args.get("city", "北京") # 默认城市“北京”
page = int(request.args.get("page", 1)) # 默认第1页
page_size = int(request.args.get("page_size", 20)) # 默认每页20条
# 调用amap_utils中的search_poi方法
result = amap_api.search_poi(keywords, city, page, page_size)
# 返回JSON格式响应
return jsonify(result)
# 2. 地理编码接口(地址→经纬度)
@app.route("/api/geocode", methods=["GET"])
def get_geocode():
address = request.args.get("address") # 前端必须传入地址参数
city = request.args.get("city") # 可选参数
if not address:
return jsonify({"success": False, "error": "地址参数(address)不能为空"})
result = amap_api.geocode(address, city)
return jsonify(result)
# 3. 逆地理编码接口(经纬度→地址)
@app.route("/api/regeo", methods=["GET"])
def get_regeo():
location = request.args.get("location") # 前端必须传入经纬度(格式:lng,lat)
if not location:
return jsonify({"success": False, "error": "经纬度参数(location)不能为空"})
result = amap_api.reverse_geocode(location)
return jsonify(result)
# 启动Flask服务
if __name__ == "__main__":
# debug=True:开发阶段开启调试模式(代码修改后自动重启服务)
app.run(host="0.0.0.0", port=5000, debug=True)
API 接口详情表(前端对接指南):
接口 URL | 请求方法 | 必选参数 | 可选参数 | 返回格式 | 功能描述 |
/api/poi | GET | 无(默认搜索 “北京餐厅”) | keywords(关键词)、city(城市)、page(页码)、page_size(每页条数) | JSON | 获取指定城市、关键词的 POI 数据(如北京的餐厅列表) |
/api/geocode | GET | address(详细地址) | city(城市) | JSON | 将地址转换为经纬度(如 “北京市中关村大街 1 号”→“116.481028,39.921983”) |
/api/regeo | GET | location(经纬度,格式:lng,lat) | 无 | JSON | 将经纬度转换为详细地址(如 “116.481028,39.921983”→“北京市海淀区中关村大街 1 号”) |
测试 API 接口(用 Postman):
- 启动 Flask 服务:运行app.py,控制台显示 “Running on http://0.0.0.0:5000/”。
- 打开 Postman,发送 GET 请求:
-
- 示例 1:测试 POI 搜索接口
URL:http://localhost:5000/api/poi?keywords=咖啡馆&city=上海&page=1&page_size=10
若返回{"success":true,"data":{"total":"123","page":1,...}},说明接口正常。
-
- 示例 2:测试地理编码接口
URL:http://localhost:5000/api/geocode?address=上海市浦东新区张江高科技园区&city=上海
若返回{"success":true,"data":{"location":"121.618528,31.219342",...}},说明接口正常。
四、JavaScript 前端开发:实现网页地图显示与数据交互
前端的核心作用是:用高德地图 JS API 在网页中显示地图,并调用 Python 后端的 API 接口,将地理数据(如 POI、经纬度)可视化到地图上。本节将分 “地图初始化→数据对接→功能扩展” 三部分讲解。
4.1 前端项目结构
创建frontend文件夹,结构如下(清晰的目录便于维护):
frontend/
├─ index.html # 网页入口(地图显示、页面布局)
├─ css/
│ └─ style.css # 页面样式(美化地图容器、按钮、列表)
└─ js/
├─ map.js # 地图核心逻辑(初始化、标记POI、事件监听)
└─ api.js # 对接Python后端API(发送请求、处理响应)
4.2 步骤 1:编写 HTML 页面(基础布局)
创建index.html,包含地图容器、搜索框、POI 列表显示区域,代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Python+高德地图:周边餐饮查询工具</title>
<!-- 1. 引入高德地图JS API(替换为你的JS API Key) -->
<script type="text/javascript" src="https://webapi.amap.com/maps?v=2.0&key=你的高德地图JS API Key"></script>
<!-- 2. 引入Axios(简化异步请求) -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<!-- 3. 引入自定义CSS -->
<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<div class="container">
<!-- 顶部搜索栏 -->
<div class="search-bar">
<input type="text" id="city-input" placeholder="请输入城市(如:北京)" value="北京">
<input type="text" id="keyword-input" placeholder="请输入关键词(如:餐厅、咖啡馆)" value="餐厅">
<button id="search-btn">搜索周边</button>
<button id="clear-btn">清空标记</button>
</div>
<!-- 主体内容:左侧地图,右侧POI列表 -->
<div class="main-content">
<!-- 地图容器(必须设置宽高,否则地图无法显示) -->
<div id="map-container"></div>
<!-- POI列表容器 -->
<div class="poi-list">
<h3>搜索结果(<span id="total-count">0</span>条)</h3>
<div id="poi-items"></div> <!-- POI列表将通过JS动态生成 -->
<!-- 分页按钮 -->
<div class="pagination">
<button id="prev-page" disabled>上一页</button>
<span id="page-info">第1页 / 共0页</span>
<button id="next-page" disabled>下一页</button>
</div>
</div>
</div>
</div>
<!-- 引入自定义JS(先引入api.js,再引入map.js,保证依赖顺序) -->
<script src="./js/api.js"></script>
<script src="./js/map.js"></script>
</body>
</html>
关键说明:
- 高德地图 JS API 引入:必须在<head>中引入,且key需替换为你申请的 “JS API Key”(若 Key 错误,地图会显示空白并报错)。
- 地图容器:#map-container必须设置宽高(将在style.css中配置),否则地图无法渲染。
- 依赖顺序:先引入api.js(对接后端 API),再引入map.js(使用 API 数据),避免变量未定义错误。
4.3 步骤 2:编写 CSS 样式(美化页面)
创建css/style.css,优化页面布局和视觉效果,代码如下:
/* 重置默认样式(避免浏览器差异) */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", sans-serif;
}
body {
background-color: #f5f5f5;
padding: 20px;
}
/* 容器:限制页面宽度,居中显示 */
.container {
max-width: 1400px;
margin: 0 auto;
}
/* 搜索栏样式 */
.search-bar {
display: flex;
gap: 10px;
margin-bottom: 20px;
align-items: center;
}
.search-bar input {
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
#city-input {
width: 200px;
}
#keyword-input {
flex: 1; /* 占满剩余宽度 */
max-width: 500px;
}
.search-bar button {
padding: 10px 20px;
background-color: #3f88c5;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.search-bar button:hover {
background-color: #2a6faa;
}
#clear-btn {
background-color: #e63946;
}
#clear-btn:hover {
background-color: #c1121f;
}
/* 主体内容:地图+POI列表 */
.main-content {
display: flex;
gap: 20px;
height: 80vh; /* 占屏幕高度的80% */
}
/* 地图容器样式 */
#map-container {
flex: 2; /* 地图占2/3宽度 */
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 1; /* 确保地图在底层,不遮挡其他元素 */
}
/* POI列表容器样式 */
.poi-list {
flex: 1; /* 列表占1/3宽度 */
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 20px;
overflow-y: auto; /* 内容超出时滚动 */
}
.poi-list h3 {
margin-bottom: 15px;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
/* POI列表项样式 */
.poi-item {
padding: 15px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.3s;
}
.poi-item:hover {
background-color: #f9f9f9;
}
.poi-item h4 {
color: #3f88c5;
margin-bottom: 5px;
}
.poi-item p {
color: #666;
font-size: 14px;
margin: 3px 0;
}
/* 分页按钮样式 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin-top: 20px;
}
.pagination button {
padding: 8px 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
cursor: pointer;
font-size: 14px;
}
.pagination button:disabled {
background-color: #f5f5f5;
color: #999;
cursor: not-allowed;
}
#page-info {
color: #666;
font-size: 14px;
}
4.4 步骤 3:编写 API 对接逻辑(api.js)
创建js/api.js,封装调用 Python 后端 API 的函数,供map.js使用。代码如下:
// api.js:对接Python后端API的工具函数
const API_BASE_URL = "http://localhost:5000/api"; // 后端API基础URL
// 1. 获取POI数据(调用后端/api/poi接口)
export async function getPOIData(params) {
try {
// 发送GET请求(Axios会自动拼接参数到URL)
const response = await axios.get(`${API_BASE_URL}/poi`, { params });
return response.data; // 返回后端响应数据
} catch (error) {
console.error("获取POI数据失败:", error);
return { success: false, error: "网络异常,获取数据失败" };
}
}
// 2. 地理编码(地址→经纬度,调用后端/api/geocode接口)
export async function getGeocode(address, city = "") {
try {
const response = await axios.get(`${API_BASE_URL}/geocode`, {
params: { address, city }
});
return response.data;
} catch (error) {
console.error("地理编码失败:", error);
return { success: false, error: "网络异常,地理编码失败" };
}
}
// 3. 逆地理编码(经纬度→地址,调用后端/api/regeo接口)
export async function getRegeo(location) {
try {
const response = await axios.get(`${API_BASE_URL}/regeo`, {
params: { location }
});
return response.data;
} catch (error) {
console.error("逆地理编码失败:", error);
return { success: false, error: "网络异常,逆地理编码失败" };
}
}
关键说明:
- API_BASE_URL:后端服务的基础地址(若后端部署到服务器,需替换为服务器 IP / 域名,如http://192.168.1.100:5000/api)。
- async/await:用于处理异步请求,避免回调地狱,代码更易读。
- 错误处理:每个函数都有try/catch,捕获网络异常并返回错误信息,便于前端提示用户。
4.5 步骤 4:编写地图核心逻辑(map.js)
创建js/map.js,实现地图初始化、POI 标记显示、搜索交互等核心功能。代码如下:
// 导入api.js中的工具函数
import { getPOIData, getGeocode, getRegeo } from "./api.js";
// 全局变量(存储地图实例、POI标记等)
let map = null; // 高德地图实例
let poiMarkers = []; // 存储POI标记的数组(用于清空标记)
let currentPage = 1; // 当前页码
let totalPage = 0; // 总页数
let totalCount = 0; // 总POI数量
let currentParams = { // 当前搜索参数(关键词、城市等)
keywords: "餐厅",
city: "北京",
page_size: 10
};
// 页面加载完成后初始化地图
document.addEventListener("DOMContentLoaded", async () => {
// 1. 初始化高德地图
initMap();
// 2. 绑定搜索、清空、分页按钮的点击事件
bindEvents();
// 3. 初始加载第一页POI数据
await loadPOIData(currentParams, currentPage);
});
/**
* 1. 初始化高德地图
*/
function initMap() {
// 创建地图实例(中心点默认设为北京:116.404, 39.915)
map = new AMap.Map("map-container", {
zoom: 13, // 地图缩放级别(1-20,越大越详细)
center: [116.404, 39.915], // 地图中心点经纬度
layers: [new AMap.TileLayer.Satellite()], // 卫星图层(可替换为默认图层:AMap.TileLayer.RoadNet)
resizeEnable: true // 允许地图随容器大小变化而调整
});
// 添加地图控件(缩放控件、比例尺、定位控件)
map.addControl(new AMap.Scale()); // 比例尺控件(显示地图比例尺)
map.addControl(new AMap.Zoom()); // 缩放控件(放大/缩小地图)
map.addControl(new AMap.Geolocation({ // 定位控件(获取用户当前位置)
enableHighAccuracy: true, // 开启高精度定位
timeout: 10000, // 定位超时时间(毫秒)
buttonPosition: "RB" // 控件位置(右下角)
}));
// 监听定位成功事件(获取用户位置后,将地图中心点移到用户位置)
map.on("geolocationComplete", (data) => {
const { position } = data;
map.setCenter([position.lng, position.lat]);
console.log("用户当前位置:", position);
});
}
/**
* 2. 绑定页面交互事件(搜索、清空、分页)
*/
function bindEvents() {
// 搜索按钮点击事件
document.getElementById("search-btn").addEventListener("click", async () => {
const city = document.getElementById("city-input").value.trim();
const keywords = document.getElementById("keyword-input").value.trim();
// 校验输入(城市和关键词不能为空)
if (!city || !keywords) {
alert("请输入城市和搜索关键词!");
return;
}
// 更新当前搜索参数
currentParams = {
keywords: keywords,
city: city,
page_size: currentParams.page_size // 保持每页条数不变
};
currentPage = 1; // 重置为第一页
// 加载新的POI数据
await loadPOIData(currentParams, currentPage);
});
// 清空标记按钮点击事件
document.getElementById("clear-btn").addEventListener("click", () => {
clearPOIMarkers(); // 清空地图上的POI标记
document.getElementById("poi-items").innerHTML = ""; // 清空POI列表
document.getElementById("total-count").textContent = "0"; // 重置总条数
document.getElementById("page-info").textContent = "第1页 / 共0页"; // 重置分页信息
currentPage = 1;
totalPage = 0;
totalCount = 0;
});
// 上一页按钮点击事件
document.getElementById("prev-page").addEventListener("click", async () => {
if (currentPage > 1) {
currentPage--;
await loadPOIData(currentParams, currentPage);
}
});
// 下一页按钮点击事件
document.getElementById("next-page").addEventListener("click", async () => {
if (currentPage < totalPage) {
currentPage++;
await loadPOIData(currentParams, currentPage);
}
});
// POI列表项点击事件(通过事件委托,监听动态生成的列表项)
document.getElementById("poi-items").addEventListener("click", (e) => {
const poiItem = e.target.closest(".poi-item");
if (poiItem) {
// 获取POI的经纬度(从data-location属性中获取)
const location = poiItem.getAttribute("data-location");
if (location) {
const [lng, lat] = location.split(",").map(Number);
// 将地图中心点移到该POI,并放大到15级
map.setCenter([lng, lat]);
map.setZoom(15);
}
}
});
}
/**
* 3. 加载POI数据(调用后端API,更新地图标记和列表)
* @param {Object} params - 搜索参数(keywords, city, page_size)
* @param {number} page - 当前页码
*/
async function loadPOIData(params, page) {
// 显示加载提示
document.getElementById("poi-items").innerHTML = '<div style="text-align:center;padding:20px;">加载中...</div>';
// 调用后端API获取POI数据
const result = await getPOIData({ ...params, page });
if (result.success) {
const { total, page: currentPageRes, pois } = result.data;
totalCount = total;
currentPage = currentPageRes;
totalPage = Math.ceil(totalCount / params.page_size); // 计算总页数(向上取整)
// 更新页面UI(总条数、分页信息)
document.getElementById("total-count").textContent = totalCount;
document.getElementById("page-info").textContent = `第${currentPage}页 / 共${totalPage}页`;
// 更新分页按钮状态(是否禁用)
document.getElementById("prev-page").disabled = currentPage === 1;
document.getElementById("next-page").disabled = currentPage === totalPage;
// 清空之前的POI标记
clearPOIMarkers();
// 1. 在地图上添加POI标记
addPOIMarkers(pois);
// 2. 生成POI列表
renderPOIList(pois);
} else {
// 显示错误提示
document.getElementById("poi-items").innerHTML = `<div style="text-align:center;padding:20px;color:red;">${result.error}</div>`;
}
}
/**
* 4. 在地图上添加POI标记
* @param {Array} pois - POI数据数组
*/
function addPOIMarkers(pois) {
pois.forEach(poi => {
const { location, name, address } = poi;
const [lng, lat] = location.split(",").map(Number);
// 创建自定义图标(替换默认标记图标)
const markerIcon = new AMap.Icon({
size: new AMap.Size(32, 32), // 图标大小
image: "https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png", // 图标地址(高德默认蓝色标记)
imageSize: new AMap.Size(32, 32) // 图标图片大小
});
// 创建标记实例
const marker = new AMap.Marker({
position: [lng, lat], // 标记位置(经纬度)
icon: markerIcon, // 自定义图标
title: name, // 鼠标 hover 时显示的标题
anchor: "bottom-center" // 图标锚点(标记点对应图标底部中心)
});
// 添加信息窗口(点击标记时显示POI详情)
const infoWindow = new AMap.InfoWindow({
content: `
<div style="padding:10px;">
<h4 style="color:#3f88c5;margin-bottom:5px;">${name}</h4>
<p style="margin:3px 0;font-size:14px;">地址:${address}</p>
<p style="margin:3px 0;font-size:14px;">电话:${poi.tel}</p>
</div>
`, // 信息窗口内容
offset: new AMap.Pixel(0, -30) // 信息窗口偏移(避免遮挡标记)
});
// 绑定标记的点击事件(显示信息窗口)
marker.on("click", () => {
infoWindow.open(map, marker.getPosition());
});
// 将标记添加到地图
marker.setMap(map);
// 存储标记到数组(用于后续清空)
poiMarkers.push({ marker, infoWindow });
});
}
/**
* 5. 生成POI列表(动态渲染到页面)
* @param {Array} pois - POI数据数组
*/
function renderPOIList(pois) {
if (pois.length === 0) {
document.getElementById("poi-items").innerHTML = '<div style="text-align:center;padding:20px;">未找到相关POI数据</div>';
return;
}
// 拼接POI列表HTML
let poiHtml = "";
pois.forEach(poi => {
poiHtml += `
<div class="poi-item" data-location="${poi.location}">
<h4>${poi.name}</h4>
<p>地址:${poi.address}</p>
<p>电话:${poi.tel}</p>
${poi.distance > 0 ? `<p>距离:${poi.distance}米</p>` : ""}
</div>
`;
});
// 将HTML插入到POI列表容器
document.getElementById("poi-items").innerHTML = poiHtml;
}
/**
* 6. 清空地图上的POI标记和信息窗口
*/
function clearPOIMarkers() {
poiMarkers.forEach(({ marker, infoWindow }) => {
marker.setMap(null); // 从地图上移除标记
infoWindow.close(); // 关闭信息窗口
});
poiMarkers = []; // 清空标记数组
}
地图核心功能拆解表:
功能模块 | 关键函数 | 实现逻辑 | 前端交互效果 |
地图初始化 | initMap() | 1. 创建地图实例,设置中心点和缩放级别;2. 添加缩放、比例尺、定位控件;3. 监听定位事件 | 页面加载后显示北京地图,右下角有定位按钮,点击可获取用户位置并居中 |
POI 搜索 | loadPOIData() | 1. 调用后端/api/poi接口获取数据;2. 清空旧标记;3. 调用addPOIMarkers()和renderPOIList()更新地图和列表 | 点击 “搜索周边” 后,地图显示 POI 标记,右侧显示列表,支持分页 |
POI 标记 | addPOIMarkers() | 1. 为每个 POI 创建自定义图标标记;2. 绑定点击事件(显示信息窗口);3. 将标记添加到地图 | 地图上显示蓝色标记,鼠标 hover 显示名称,点击显示详情(地址、电话) |
POI 列表交互 | renderPOIList() + 事件委托 | 1. 动态生成 POI 列表 HTML;2. 列表项绑定点击事件(地图居中到该 POI) | 点击右侧列表项,地图自动定位到对应 POI 并放大 |
清空标记 | clearPOIMarkers() | 1. 遍历标记数组,移除地图上的标记;2. 关闭信息窗口;3. 清空数组 | 点击 “清空标记” 后,地图标记和列表全部清空 |
五、功能扩展:从基础到进阶(可选)
基础版本实现后,你可以根据需求扩展功能。以下是几个常用的进阶功能,同样用表格和代码说明实现思路。
5.1 扩展 1:添加 “地址搜索定位” 功能
需求:用户输入详细地址(如 “上海市浦东新区张江高科技园区”),点击按钮后地图定位到该地址,并显示 POI。
实现步骤:
- 在index.html的搜索栏添加地址输入框和定位按钮:
<div class="search-bar">
<!-- 原有代码 -->
<input type="text" id="address-input" placeholder="请输入详细地址(如:北京市中关村大街1号)">
<button id="locate-address-btn">地址定位</button>
</div>
- 在map.js的bindEvents()函数中添加按钮点击事件:
// 地址定位按钮点击事件
document.getElementById("locate-address-btn").addEventListener("click", async () => {
const address = document.getElementById("address-input").value.trim();
const city = document.getElementById("city-input").value.trim();
if (!address) {
alert("请输入详细地址!");
return;
}
// 调用地理编码接口(地址→经纬度)
const result = await getGeocode(address, city);
if (result.success) {
const { location, formatted_address } = result.data;
const [lng, lat] = location.split(",").map(Number);
// 1. 地图定位到该地址
map.setCenter([lng, lat]);
map.setZoom(15);
// 2. 在该地址添加定位标记
clearPOIMarkers(); // 清空原有标记
const locateMarker = new AMap.Marker({
position: [lng, lat],
icon: new AMap.Icon({
size: new AMap.Size(32, 32),
image: "https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png", // 红色定位标记
imageSize: new AMap.Size(32, 32)
}),
title: "定位地址"
});
locateMarker.setMap(map);
poiMarkers.push({ marker: locateMarker, infoWindow: new AMap.InfoWindow() });
// 3. 显示地址信息窗口
const infoWindow = new AMap.InfoWindow({
content: `<div style="padding:10px;">定位地址:${formatted_address}</div>`,
offset: new AMap.Pixel(0, -30)
});
infoWindow.open(map, [lng, lat]);
// 4. 可选:搜索该地址周边的POI(如周边500米内的餐厅)
currentParams = {
keywords: currentParams.keywords,
city: city,
page_size: currentParams.page_size,
location: location, // 新增:以定位地址为中心点
radius: 500 // 新增:搜索半径500米
};
currentPage = 1;
await loadPOIData(currentParams, currentPage);
} else {
alert(`地址定位失败:${result.error}`);
}
});
- 更新 Python 后端amap_utils.py的search_poi()方法,支持按中心点和半径搜索:
def search_poi(self, keywords, city, page=1, page_size=20, location=None, radius=None):
url = f"{self.base_url}/place/text"
params = {
"key": self.api_key,
"keywords": keywords,
"city": city,
"page": page,
"offset": page_size,
"output": "json",
"extensions": "all"
}
# 新增:若传入location和radius,添加到请求参数(按中心点搜索)
if location and radius:
params["location"] = location
params["radius"] = radius # 搜索半径(米)
# 后续代码不变...
5.2 扩展 2:添加 “热力图” 显示 POI 分布
需求:用热力图直观展示 POI 的分布密度(如北京餐厅的分布情况)。
实现步骤:
- 在index.html的高德地图 JS API 引入中添加热力图插件:
<script type="text/javascript" src="https://webapi.amap.com/maps?v=2.0&key=你的JS API Key&plugins=HeatMap"></script>
- 在map.js中添加热力图相关全局变量和函数:
let heatMap = null; // 热力图实例
let isHeatMapShow = false; // 热力图是否显示的开关
// 在initMap()函数中初始化热力图(默认不显示)
function initMap() {
// 原有代码...
// 初始化热力图(隐藏状态)
heatMap = new AMap.HeatMap(map, {
radius: 25, // 热力图点半径(越大,热力范围越广)
opacity: [0, 0.8], // 透明度范围
gradient: { // 热力图颜色梯度(从低到高:蓝→绿→黄→红)
0.2: '#00fffc',
0.4: '#4dff4d',
0.6: '#ffff00',
0.8: '#ff9900',
1.0: '#ff0000'
}
});
heatMap.hide(); // 默认隐藏热力图
}
// 添加热力图切换按钮事件(在bindEvents()中)
function bindEvents() {
// 原有代码...
// 热力图切换按钮(需在index.html中添加按钮)
document.getElementById("toggle-heatmap-btn").addEventListener("click", () => {
isHeatMapShow = !isHeatMapShow;
const btn = document.getElementById("toggle-heatmap-btn");
if (isHeatMapShow) {
// 显示热力图:隐藏POI标记,显示热力图
clearPOIMarkers();
heatMap.show();
btn.textContent = "隐藏热力图";
// 加载POI数据并生成热力图数据
if (totalCount > 0) {
loadHeatMapData(currentParams);
} else {
alert("请先搜索POI数据,再显示热力图!");
isHeatMapShow = false;
btn.textContent = "显示热力图";
}
} else {
// 隐藏热力图:显示POI标记
heatMap.hide();
btn.textContent = "显示热力图";
loadPOIData(currentParams, currentPage); // 重新加载POI标记
}
});
}
// 加载热力图数据
async function loadHeatMapData(params) {
// 获取所有POI数据(不分页,最多1000条,高德API限制)
const result = await getPOIData({ ...params, page: 1, page_size: 1000 });
if (result.success) {
const pois = result.data.pois;
// 转换为热力图所需格式:[[lng, lat, weight], ...](weight为权重,这里用1)
const heatData = pois.map(poi => {
const [lng, lat] = poi.location.split(",").map(Number);
return [lng, lat, 1]; // 权重为1,所有POI同等重要
});
// 设置热力图数据
heatMap.setData(heatData);
}
}
- 在index.html的搜索栏添加热力图切换按钮:
<div class="search-bar">
<!-- 原有代码 -->
<button id="toggle-heatmap-btn">显示热力图</button>
</div>
六、常见问题排查与优化(避坑指南)
在开发过程中,你可能会遇到各种问题(如地图空白、API 调用失败)。下表整理了高频问题及解决方案:
常见问题 | 可能原因 | 解决方案 |
地图显示空白,控制台报错 “Invalid Key” | 1. JS API Key 错误;2. Key 未勾选 “JS API” 权限;3. Referer 白名单未设置 | 1. 检查 Key 是否为 “JS API Key”,而非 “Web 服务 API Key”;2. 进入高德开发者平台,确认 Key 已勾选 “JS API” 权限;3. 开发阶段将 Referer 白名单设为localhost或* |
前端调用后端 API 报错 “CORS Error” | 后端未配置跨域,浏览器拦截前端请求 | 1. 确保 Flask 已安装flask-cors;2. 在app.py中添加CORS(app, resources=r"/*");3. 生产环境需限制允许跨域的域名(如CORS(app, resources={"r/*": {"origins": "https://www.example.com"}})) |
后端调用高德 Web 服务 API 返回 “INVALID_USER_KEY” | 1. Web 服务 API Key 错误;2. Key 未勾选 “Web 服务” 权限;3. Key 已过期 | 1. 检查.env文件中的AMAP_WEB_KEY是否正确;2. 确认 Key 已勾选 “Web 服务” 权限;3. 进入高德开发者平台,检查 Key 是否有效(未被禁用) |
地图标记不显示,但 POI 列表正常 | 1. 经纬度格式错误(如 “lat,lng” 顺序颠倒);2. 标记图标地址错误;3. 地图容器未设置宽高 | 1. 确认 POI 的location格式为 “lng,lat”(如 “116.404,39.915”);2. 检查addPOIMarkers()中的图标地址是否可访问;3. 确认#map-container在 CSS 中设置了width和height |
前端请求后端 API 时,控制台显示 “404 Not Found” | 1. 后端服务未启动;2. API URL 错误;3. 请求方法错误(如用 POST 请求 GET 接口) | 1. 确保app.py已启动,且端口为 5000;2. 检查api.js中的API_BASE_URL是否为http://localhost:5000/api;3. 确认请求方法与后端接口一致(如/api/poi为 GET 请求) |
热力图不显示,控制台无报错 | 1. 热力图插件未引入;2. 热力图数据格式错误;3. 热力图被 POI 标记覆盖 | 1. 确认 JS API 引入时添加了plugins=HeatMap;2. 检查heatData格式是否为[[lng, lat, weight], ...];3. 显示热力图前需调用clearPOIMarkers()清空标记 |
七、项目部署:从本地到线上(可选)
本地开发完成后,若想让他人访问你的地图应用,需要将项目部署到服务器。以下是简易部署方案(适合个人测试):
7.1 部署 Python 后端(用 PythonAnywhere)
- 注册PythonAnywhere账号(免费版支持 1 个 Web 应用)。
- 上传后端代码(app.py、amap_utils.py、.env)到 PythonAnywhere 的mysite目录。
- 安装依赖:在 PythonAnywhere 的 “Consoles” 中运行pip install flask requests flask-cors python-dotenv。
- 配置 Web 应用:
-
- 选择 “Flask” 框架,Python 版本 3.9+。
-
- 设置 “Source code” 路径为你的代码目录(如/home/你的用户名/mysite)。
-
- 设置 “WSGI configuration file” 为/home/你的用户名/mysite/flask_app.wsgi。
- 启动 Web 应用:点击 “Reload”,后端 API 地址为https://你的用户名.pythonanywhere.com/api。
7.2 部署前端页面(用 GitHub Pages)
- 创建 GitHub 仓库(如amap-demo-frontend),上传前端代码(index.html、css/、js/)。
- 进入仓库 “Settings”→“Pages”,设置 “Source” 为 “main branch”,“Folder” 为 “/”。
- 保存后,前端页面地址为https://你的用户名.github.io/amap-demo-frontend/。
- 修改前端api.js中的API_BASE_URL为 PythonAnywhere 的后端地址(如https://你的用户名.pythonanywhere.com/api)。
八、总结与展望
8.1 项目总结
本文通过 “周边餐饮查询工具” 实战案例,完整讲解了 “Python 后端 + 高德地图 + JavaScript 前端” 的开发流程,核心收获如下:
- 技术栈整合:Python(Flask)负责后端数据处理和 API 封装,高德地图提供地理数据和地图渲染能力,JavaScript 负责前端交互,三者协同实现 “数据→接口→可视化” 的完整链路。
- 关键知识点:
-
- 高德地图 API 的两种 Key(Web 服务 API Key 用于后端,JS API Key 用于前端)的申请与配置。
-
- Python 后端如何调用第三方 API(高德 Web 服务 API)并封装成 HTTP 接口。
-
- JavaScript 前端如何用高德 JS API 初始化地图、添加标记、实现交互,并对接后端 API。
- 实战能力:掌握了 POI 搜索、地理编码、地图标记、热力图等常用功能的实现,以及跨域问题、API 错误排查等实战技巧。
8.2 未来扩展方向
- 功能扩展:
-
- 添加 “路径规划” 功能(步行、驾车、公交路线)。
-
- 结合数据库(如 MySQL)存储 POI 数据,支持历史搜索记录。
-
- 实现用户认证(如微信登录),保存用户的收藏 POI。
- 性能优化:
-
- 后端添加缓存(如 Redis),减少重复调用高德 API 的次数(节省额度)。
-
- 前端实现 POI 数据懒加载,优化大数据量下的渲染速度。
- 平台扩展:
-
- 基于此项目开发移动端应用(用 React Native 或 Flutter)。
-
- 集成其他地图服务(如百度地图、谷歌地图),支持多地图切换。