欢迎大家来到IT世界,在知识的湖畔探索吧!
最近刷到一篇热帖,一位在阿里五年的同学总结了一份她这五年(吃的每一口记录)收集的杭州美食排行榜,为“美食荒漠”正名。也让我回忆起自己初到杭州进入团队时也曾想整理一份“美食地图”,却因种种原因而作罢。恰逢最近团队准备选团建的餐厅,于是立马把这篇帖子分享给投食官。投食官仔细调研后感慨道,“这篇文章真是太好了,如果能在地图上看到它们的位置就更好了。”
这让我萌生了为这个美食排行榜做一个杭州美食地图的想法,以便能快速地看到餐厅的位置并进行选择。实现方法上 AI coding 加高德 MCP 有没有搞头?
文末附源码和项目地址供大家参考和取用。
安装高德MCP server
过程参考 :Cursor快速接入高德MCP server
https://lbs.amap.com/api/mcp-server/gettingstarted
只需准备好你的高德 key,在 cursor 上设置里如下配置即可。
{ "mcpServers": { "amap-amap-sse": { "url": "https://mcp.amap.com/sse?key=您在高德官网上申请的key" } } }
欢迎大家来到IT世界,在知识的湖畔探索吧!
配置好后检查 mcpserver 的状态,如下图则是正常可用的。
欢迎大家来到IT世界,在知识的湖畔探索吧!
给 AI 提需求-构建工程提示词
欢迎大家来到IT世界,在知识的湖畔探索吧!1. 在当前文件夹下进行编写项目,尽量少的创建文件,只创建必要文件。
2. 根据给定的杭州推荐的美食店,制作一份可以预览的美食地图。最终的产物使用便捷的方式呈现,如使用html进行展示。
3. 地图上标注下面每个店铺精确的位置,为每个类型的餐厅用不同的合适的图标表示。
4. 地图采用真实的杭州市地图。
5. 点击图标可显示餐厅更多信息,餐厅优势,预约电话,推荐指数等。
6. 过程中可以借助任何工具和能力实现这一目标,如 高德MCP工具。
7. 展示的店铺已给定的为准,最终给出的数据要真实可靠。
8.将下列数据,通过amp-amp-sse 的 mcpserver,制作成适配高德地图的 json 格式数据。若没有找到餐厅,不要编造数据,坐标字段设为空值即可。
餐厅数据如下:
如*:主厨团队对传统杭帮菜的解构能力堪称艺术,招牌鱼圆弹润如凝脂,黄酒鹅肝配牛小排的组合既有东方韵味又有西式巧思。景观位虽为日式下沉设计,但私密性极佳,适合重要纪念日。唯一提醒:订位时建议直接对接主理人确认细节,米其林加身后服务响应速度有提升空间。
金*厅:四季酒店的镇店之宝,时令菜单每月更新,清明前的香椿嫩芽配河虾仁、霜降后的桂花糖藕都是必尝限定。脆皮鸡的表皮酥脆度堪比艺术品,和牛炒饭的镬气十足,适合带海外友人感受中式烹饪的精细度。
八*杯:里园的孪生品牌,早期的黄鱼锅贴仍是教科书级别,但近年创新菜式如松茸酿饭口感层次稍显单薄。建议避开周末午市,工作日晚间用餐能更好体会主厨团队的匠心。
集*楼:庭院里的江南美学展,青梅鹅肝配茉莉花茶冻的创意值得点赞。近期推出的「盲盒套餐」性价比突出,适合想体验高端杭菜又怕踩雷的新客。
木*溪:宋宴主题套餐需提前三天预订,茶点与菜肴的搭配暗合二十四节气,但每道菜量控制在2-3口刚好尝鲜。特别推荐茶艺师现场点茶,宋代七汤点茶法视觉效果震撼。
桂*房:满觉陇店的露台夜景是最大卖点,桂花酿醉虾的甜度把控精准。若多人聚餐可尝试「山房九宫格」拼盘,冷热荤素搭配考究,但需注意部分创意菜式如黑松露猫耳朵口味较重。
【浙菜】
锦*湖:原乾胧汇班底升级之作,龙翔桥店的脆皮鸡改良自港式烧味,外皮的焦糖层厚度比EFC店更均匀。推荐搭配店家自酿的青梅酒,解腻效果出众。
算*挡:滨江园区的宝藏食堂,人均150可吃到现拆的蟹粉豆腐和炭烤鳗鱼。建议4人以上拼单,把招牌菜凑齐分着吃更划算。
【创意菜】
雅*:西溪湿地内的隐秘据点,荷叶包蒸东山羊是夏季限定,肉质软烂入味。划船至码头需提前预约接驳船,整体体验像私人会所。
【融合菜】
乔*:阿里园区周边的老牌宴请场所,最近推出的「数字化点餐系统」可实时查看后厨烹饪进度。推荐黑椒牛排配龙井茶汤,西式主菜与杭帮调味的结合恰到好处。
略...
输入“需求文档”后,cursor 会进入 Plan 模式进行规划整体设计方案。
已开始为你的美食地图收集真实的高德地图数据。以下是实现方案和进度说明:
实现方案
数据收集:为每个餐厅通过高德地图API检索真实的地理位置(经纬度)、地址、照片等信息。若未找到则坐标为空。
数据格式:整理为适配高德地图MCP的JSON格式,包含必要的字段(如名称、类型、坐标、推荐指数、电话、优势等)。
地图展示:最终生成一个HTML文件,使用高德地图JS API进行可视化。不同类型餐厅用不同图标,点击可弹窗显示详细信息。
文件结构:只创建必要的文件(如index.html和数据JSON),保证项目简洁。
进度说明
已完成“如*”“金*厅”真实数据检索,获取了精确坐标和相关信息。
正在依次为所有餐厅检索真实数据,确保数据可靠。
检索不到的餐厅将坐标设为空,不会编造信息。
请确认:
是否需要为每家餐厅都补充“推荐指数、预约电话、优势”等字段?如无公开数据,是否允许留空或用描述性文字?
是否需要将所有数据单独导出为JSON文件,还是直接嵌入HTML中?
如无异议,将继续批量检索并整理数据,随后生成地图预览HTML。你可随时查看进度或指定展示细节。
十几分钟写完“需求文档”,晚饭时间到,提给 AI 让它先写着,我们去吃饭~
AI coding-开始寻找杭州美食之旅
调用高德 MCP Server-获取餐厅准确位置和信息
第一轮-模糊查询找到候选餐厅
AI 会先调用高德 MCP Server 的 maps_text_search 方法通过模糊查询,查到候选餐厅信息。
输入
欢迎大家来到IT世界,在知识的湖畔探索吧!{ "keywords": "如院餐厅 杭州", "city": "杭州", "citylimit": true }
输出
可以看到,查出了一系列餐厅,其中有如*餐厅的基本信息,取出 id,作为下一轮的输入。
{ "pois": [ ... { "id": "B0JA5UF6R*", "name": "如*餐厅", "address": "玉泉路1*号", "typecode": "0501", "photo": "http://store.is.autonavi.com/showpic/b08795ab6adeba25a0115d" }, { "id": "B023B019A*", "name": "群*饭店(信*路店)", "address": "滨安路119*号(近信*路)", "typecode": "0501", "photo": "http://store.is.autonavi.com/showpic/2ac9e95efffe6e27a8eda43ef82711" }, ... ], "suggestion": { "keywords": "", "ciytes": { "suggestion": [] } } }
第二轮-通过 id 获取餐厅详情
可以看到紧接着 AI 会调用高德 MCP Server 的maps_search_detail能力,通过 id 来获取餐厅详情。
输入
欢迎大家来到IT世界,在知识的湖畔探索吧!{ "id": "B0JA5UF6R*" }
输出
可以看到这里获取到了餐厅的详情信息,包含名称、经纬度、地址、照片、营业时间、评分等。
{"id":"B0JA5UF6R*","name":"如*餐厅","location":"120.,30.","address":"玉泉路1*号","business_area":"西湖旅游区中心片区","city":"杭州市","type":"餐饮服务;中餐厅;中餐厅","alias":"","photo":"http://store.is.autonavi.com/showpic/b08795ab6adeba25a0115da*","cost":"","opentime2":"周一至周日 11:30-14:00,17:30-21:00","rating":"4.7","open_time":"11:30-14:00 17:30-21:00","meal_ordering":"0"}
AI 会将高德 MCP Server 中获取的数据格式化,整理到 json 文件中,作为数据源。
编写页面
数据收集完后,就开始根据需求,编写代码了,通过引入高德 API 展示杭州地图,然后将上一步整理的数据引入进来,标出餐厅的位置。并且支持点击餐厅的名称,弹框展示餐厅的详情,餐厅的推荐理由。
虽然没有关注代码具体怎么实现的,但是它就这样水灵灵的工作起来了~
但我认为我们还是需要理解 AI 写的每一行代码,对代码质量有一个评估,一方面自己能有所进步,一方面不能让 AI 自由发挥的太过,到后面一行也看不懂,改也改不动的时候,项目就崩了。自己不理解的代码,可以让 AI 为代码都加上注释,方便理解。
调试页面
AI 写完了主体页面和功能点后,预览一下,还是有挺多细节需要修改的,如名称大小、背景框样式、弹框点击关闭逻辑等,这些和 AI 沟通,说明需要修改的位置和要求即可,如果说不清的时候,可以直接截图给 AI,告诉它要改什么,理解意图上还是十分强大的。
找素材
但是 AI 有一点不足的是不太方便从外部直接获取资源,比如试图让 AI 自己去网上找合适的素材图标,但是失败了,它会让我去给它下载,不然它会自己画一个丑陋抽象的 SVG 图标。
实在忍不了,只能去网上找素材,可以说整个过程中,花的最多的时间就是这里了。找合适的素材,尝试下载,要收费?换一个地方找,终于是免费的了,下载下来,传到图床上,变成公网可访问的链接。
整个找素材的过程十分痛苦,如果 AI 可以自己找就好了,也许是使用的姿势不太对,还需要继续研究。
好在最终找到了一个非常棒的平台,那就是阿里巴巴图标矢量库,还是自家兄弟好,素材非常的全且好用,大力推荐。
地址:https://www.iconfont.cn/?spm=a313x.search_index.i3.d4d0a486a.2dff3a81XH4Q3q
餐厅分类与筛选
对于之前的收集的餐厅的数据,没有一个很好的分类进行表达,于是让AI 重新分析一下整个餐厅数据,结合餐厅的信息,进行分类,然后将餐厅类型与图标素材再进行一个匹配。得到一个分类好的餐厅列表,与图标的映射关系。
然后就可以愉快的让 AI 结合这个分类数据,改写一下餐厅的图标啦,并且基于分类,再加上了图例列表,和筛选项的功能,方便展示,不然 100个餐厅全部展示在地图上,也不太方便小伙伴们看。
然后页面的效果如下:
AI 推荐餐厅
看了地图上标注的餐厅后,才发现,离自己园区的距离都比较远,于是含泪再写一份自己团队的聚餐选点指南,让 AI 帮助推荐餐厅。
欢迎大家来到IT世界,在知识的湖畔探索吧!1. 在当前项目中进行改进,删去图例、筛选项功能。 2. 使用给出的餐厅数据,在地图上进行标注。 3. 我们的出发位置是杭州市阿里巴巴滨江园区,标注出发点位置,用星星图标表示。 4. 图标素材在 resource/icon.txt中. 5. 计算出发点到各个餐厅的位置,在地图上用线连起来进行展示。 6. 分析一下各个餐厅的优劣,离出发点的距离,交通方式,和所需时间,周边配套,列在餐厅详情里。 7. 最后进行一个智能总结,推荐一个餐厅 8. 9. 过程中可以借助任何工具和能力实现这一目标,如 高德MCP工具。 10. 展示的店铺已给定的为准,最终给出的数据要真实可靠。 将下列数据,通过amp-amp-sse 的 mcpserver,制作成适配高德地图的 json 格式数据。若没有找到餐厅,不要编造数据,坐标字段设为空值即可。 1. 海*捞中南乐游城店 2. 黄*海鲜大排档(长河地铁站旁) 3. 群*饭店 4. 龙*·滨江烤全羊(杭州总店)dong'da 5. 东*方保利时光里店
有了前面的基础,这次 20 分钟就搞定了,主要在写需求文档和微调页面样式上,可以分析一下各个餐厅的优劣,离出发点的距离,交通方式,和所需时间,周边配套等。 最后进行一个智能总结,推荐一个餐厅,效果如下:
小伙伴们就可以愉快的进行决策,到底选哪个餐厅了~
新的功能正在和我们投食官持续优化中,比如先选择团建地方,再选择聚餐地方,多个地点让 AI 分析推荐等~
发布与部署
公网部署方法挺多的,比如 GitHub、Gitee上提供了静态页面部署的能力,或者可以租一个阿里云服务器申请域名,搭建自己的网站。
在 GitHub 部署静态页面的简要指南
以下为 GitHub Pages 静态部署的核心步骤,基于官方文档和工具链实现:
1.创建仓库
- 新建公共仓库,命名格式为 username.github.io(username 为你的 GitHub 用户名)。
- 克隆仓库到本地,添加静态文件(如 index.html),并推送至 main 分支。
2. 配置发布源
- 进入仓库 Settings > Pages,选择发布分支(如 main 或 gh-pages)或 docs 文件夹作为源。
- 若使用自定义构建工具(如 Hugo、Vue 等),需禁用默认 Jekyll 构建:在源目录根路径创建空文件 .nojekyll。
3. 使用 GitHub Actions 自动部署
- 在工作流配置文件(如 .github/workflows/deploy.yml)中添加以下步骤:
- name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./public # 替换为你的构建输出目录
此 Action 会将指定目录内容部署到 gh-pages 分支。
4.访问页面
- 页面地址为 https://username.github.io,更新后通常需等待 1-10 分钟生效。
5. 注意事项
- 权限问题:若使用 GitHub Actions 推送,需确保管理员已验证邮箱并推送过代码。
- 符号链接:仓库含符号链接时,需通过 GitHub Actions 部署。
最后
这次的实践我认为还是非常的有趣的,整个开发过程大概 3-4 小时,而且主要花在了找素材和调整样式上了。放在以前,有了一个好的点子,但是可能会因为麻烦,跨技术栈,生产周期长、拖延症等各种因素导致“创业未半而中道崩殂”。
不得不感叹,AI 真的能帮助人们提高效率,并且使之前不可能或者很难完成的事情变得可行。在 AI 时代,可能需要从之前传统的“关注技术栈和专业,关注如何实现”的思维转向聚焦到“扩展思维的宽度和认知,寻找事物的可达路径”上。
另外,高德的小伙伴是否考虑一下,增加一个餐厅、团建的地点投票功能~
可以在高德上发起投票,录入几个候选位置,邀请小伙伴们一起投票。可以很方便的查看当前位置(或者出发地点)距离候选地点的距离、交通方式和用时、候选地点周边配套等。 也可以让 AI 结合需求背景,总结一下各候选地点的优势。然后小伙伴们就可以愉快地投票了!
附录
源码如下,需要的小伙伴可自取,在这个基础上改改就能用了。
杭州美食地图
欢迎大家来到IT世界,在知识的湖畔探索吧!<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>杭州美食地图</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
html, body, #map { height: 100%; margin: 0; padding: 0; }
.amap-info-content { font-size: 14px; }
.legend {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255,255,255,0.9);
border-radius: 6px;
padding: 10px;
box-shadow: 02px 8px rgba(0,0,0,0.15);
z-index: 1000;
}
.legend-item { display: flex; align-items: center; margin-bottom: 6px; }
.legend-icon { width: 20px; height: 20px; margin-right: 6px; }
.marker-label {
font-size: 16px;
color: #fff;
background: rgba(34,34,34,0.75);
border-radius: 6px;
padding: 2px 10px;
box-shadow: 02px 8px rgba(0,0,0,0.15);
white-space: nowrap;
transition: background 0.2s;
border: none;
}
.marker-label-hover {
background: rgba(34,34,34,0.95);
color: #ffe58f;
border: none;
}
/* 强制覆盖高德地图label的蓝色边框和背景 */
.amap-marker-label {
border: none !important;
box-shadow: none !important;
background: none !important;
padding: 0 !important;
}
.filter-panel {
position: absolute;
top: 40px;
right: 20px;
width: 220px;
background: rgba(255,255,255,0.97);
border-radius: 10px;
box-shadow: 02px 16px rgba(0,0,0,0.13);
padding: 18px 18px 12px 18px;
z-index: 1200;
font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
font-size: 15px;
color: #222;
transition: box-shadow 0.2s;
max-height: 600px;
overflow-y: auto;
}
.filter-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 12px;
letter-spacing: 2px;
}
.filter-section { margin-bottom: 14px; }
.filter-label { font-size: 14px; margin-bottom: 6px; color: #666; }
.filter-recommend label, .filter-category label {
display: inline-block;
margin-right: 10px;
margin-bottom: 6px;
cursor: pointer;
font-size: 14px;
}
.filter-category {
max-height: 280px;
overflow-y: auto;
border-radius: 6px;
background: #f7f7f7;
padding: 6px 4px 4px 8px;
border: 1px solid #eee;
}
.filter-category label { width: 120px; }
.filter-panel input[type="checkbox"] {
accent-color: #1890ff;
margin-right: 3px;
vertical-align: middle;
}
</style>
<!-- 高德地图 JS API -->
<script src="https://webapi.amap.com/maps?v=2.0&key=「你的高德 apikey」"></script>
</head>
<body>
<div id="map"></div>
<div class="legend" id="legend"></div>
<div id="filter-panel" class="filter-panel">
<div class="filter-title">杭州美食指南</div>
<div class="filter-section">
<div class="filter-label">推荐指数</div>
<div class="filter-recommend">
<label><input type="checkbox" name="recommend" value="5"> 5星</label>
<label><input type="checkbox" name="recommend" value="4"> 4星</label>
<label><input type="checkbox" name="recommend" value="3"> 3星</label>
<label><input type="checkbox" name="recommend" value="2"> 2星</label>
<label><input type="checkbox" name="recommend" value="1"> 1星</label>
</div>
</div>
<div class="filter-section">
<div class="filter-label">餐厅类别</div>
<div class="filter-category" id="filter-category-list"></div>
</div>
</div>
<script>
// 餐厅数据直接嵌入或读取同级目录下文件都可
const data = [
{
"name": "如*餐厅",
"type": "中餐厅",
"location": "120.122175,30.253356",
"address": "玉泉路1*号",
"photo": "http://store.is.autonavi.com/showpic/b08795ab6adeba25a0115d74130862aa",
"cost": "",
"open_time": "11:30-14:00 17:30-21:00",
"rating": "4.7",
"phone": "",
"advantage": "推荐理由 xxxx",
"recommend_index": "5",
"category": "中餐"
},
{
"name": "杭州湖四季酒店金沙厅",
"type": "浙江菜",
"location": "120.129637,30.251887",
"address": "灵隐路5号杭州湖四季酒店1层(近曲院风荷)",
"photo": "http://store.is.autonavi.com/showpic/03f435393b04359460529d3cc484e826",
"cost": "533.00",
"open_time": "11:30-14:00 17:30-21:00",
"rating": "4.8",
"phone": "",
"advantage": "推荐理由 xxxx",
"recommend_index": "5",
"category": "杭帮菜/浙江菜"
},
{
"name": "门没锁咖啡馆",
"type": "咖啡馆",
"location": "120.099355,30.297518",
"address": "政紫弄23幢号沿街(地铁站F口步行210米)",
"photo": "https://aos-comment.amap.com/B0KUPD8084/comment/A241830B_F0B4_4496_A253_95A1F5385C51_L0_001_1500_2000_1735672012627_76019873.jpg",
"cost": "",
"open_time": "10:00-17:00",
"rating": "4.3",
"phone": "",
"advantage": "推荐理由 xxxx",
"recommend_index": "4",
"category": "咖啡馆"
}
{
"name": "helloworld",
"type": "咖啡馆",
"location": "120.005480,30.280547",
"address": "欧美金融城西溪丽晶居*幢底商1-13号",
"photo": "http://store.is.autonavi.com/showpic/72efe174477c732d4264f7373dfaf768",
"cost": "30.00",
"open_time": "08:00-18:00",
"rating": "4.3",
"phone": "",
"advantage": "推荐理由 xxxx",
"recommend_index": "4",
"category": "咖啡馆"
}
];
// 图标映射表,按最新icon.txt
const typeIcons = {
'辣椒': '「需要自己准备」',
'甜点': '「需要自己准备」',
'咖啡': '「需要自己准备」',
'中餐': '「需要自己准备」',
'日料': '「需要自己准备」',
'快餐': '「需要自己准备」',
'烤肉': '「需要自己准备」',
'烧烤': '「需要自己准备」',
'星星': '「需要自己准备」',
'火锅': '「需要自己准备」',
'牛排': '「需要自己准备」',
'面食': '「需要自己准备」',
'海鲜': '「需要自己准备」',
'韩餐': '「需要自己准备」',
'小吃': '「需要自己准备」',
'粤菜': '「需要自己准备」',
'default': '「需要自己准备」'
};
// 图例分类,按icon.txt顺序
const legendTypes = [
{type: '中餐', label: '中餐/杭帮菜/其他'},
{type: '粤菜', label: '粤菜'},
{type: '辣椒', label: '川菜'},
{type: '火锅', label: '火锅'},
{type: '烧烤', label: '烧烤'},
{type: '烤肉', label: '烤肉'},
{type: '日料', label: '日料'},
{type: '韩餐', label: '韩餐'},
{type: '牛排', label: '西餐'},
{type: '面食', label: '面馆/面食'},
{type: '小吃', label: '小吃'},
{type: '甜点', label: '甜品/饮品'},
{type: '咖啡', label: '咖啡馆'},
{type: '快餐', label: '快餐'},
{type: '海鲜', label: '海鲜'}
];
// category到icon的映射规则
function getCategoryIcon(category){
if (!category) return typeIcons['default'];
if (category === '中餐' || category === '杭帮菜/浙江菜' || category === '湘菜' || category === '东北菜' || category === '素食' || category === '私房菜') return typeIcons['中餐'];
if (category === '粤菜') return typeIcons['粤菜'];
if (category === '川菜') return typeIcons['辣椒'];
if (category === '火锅') return typeIcons['火锅'];
if (category === '烧烤/烤肉') return typeIcons['烤肉'];
if (category === '烧烤') return typeIcons['烧烤'];
if (category === '日料') return typeIcons['日料'];
if (category === '韩餐') return typeIcons['韩餐'];
if (category === '西餐') return typeIcons['牛排'];
if (category === '面馆') return typeIcons['面食'];
if (category === '小吃') return typeIcons['小吃'];
if (category === '甜品/饮品') return typeIcons['甜点'];
if (category === '咖啡馆') return typeIcons['咖啡'];
if (category === '快餐') return typeIcons['快餐'];
if (category === '海鲜') return typeIcons['海鲜'];
return typeIcons['default'];
}
// 初始化地图
constmap = new AMap.Map('map', {
center: [120.15507, 30.274085], // 杭州中心
zoom: 12
});
// 移除所有 data.forEach(...) 相关的marker渲染,只保留 renderMarkers() 控制
// ... existing code ...
// 图例渲染
const legend = document.getElementById('legend');
legend.innerHTML = legendTypes.map(t =>
`<div class="legend-item"><img class='legend-icon' src='${typeIcons[t.type]}' alt='${t.label}' />${t.label}</div>`
).join('');
// InfoWindow全局变量
let infoWindow = null;
// label样式
const style = document.createElement('style');
style.innerHTML = `
.marker-label {
font-size: 16px;
color: #fff;
background: rgba(34,34,34,0.75);
border-radius: 6px;
padding: 2px 10px;
box-shadow: 02px 8px rgba(0,0,0,0.15);
white-space: nowrap;
transition: background 0.2s;
border: none;
}
.marker-label-hover {
background: rgba(34,34,34,0.95);
color: #ffe58f;
border: none;
}
/* 强制覆盖高德地图label的蓝色边框和背景 */
.amap-marker-label {
border: none !important;
box-shadow: none !important;
background: none !important;
padding: 0 !important;
}
`;
document.head.appendChild(style);
// 动态生成类别筛选项
const allCategories = Array.from(new Set(data.map(d => d.category).filter(Boolean)));
const filterCategoryList = document.getElementById('filter-category-list');
filterCategoryList.innerHTML = allCategories.map(cat =>
`<label><input type='checkbox' name='category' value='${cat}'> ${cat}</label>`
).join('');
// 默认勾选5星
document.querySelector('input[name="recommend"][value="5"]').checked = true;
// 筛选逻辑
function getSelectedRecommends() {
return Array.from(document.querySelectorAll('input[name="recommend"]:checked')).map(i => i.value);
}
function getSelectedCategories(){
return Array.from(document.querySelectorAll('input[name="category"]:checked')).map(i => i.value);
}
let allMarkers = [];
function renderMarkers(){
// 清除旧marker
allMarkers.forEach(m => m.setMap(null));
allMarkers = [];
const selectedRecommends = getSelectedRecommends();
const selectedCategories = getSelectedCategories();
let filtered = data;
// 取交集策略:都选了才展示
if (selectedRecommends.length) {
filtered = filtered.filter(d => selectedRecommends.includes(d.recommend_index));
}
if (selectedCategories.length) {
filtered = filtered.filter(d => selectedCategories.includes(d.category));
}
// 如果都没选,展示全部
if (!selectedRecommends.length && !selectedCategories.length) {
filtered = data;
}
filtered.forEach(item => {
if (!item.location) return;
const [lng, lat] = item.location.split(',').map(Number);
const iconUrl = getCategoryIcon(item.category);
const marker = new AMap.Marker({
position: [lng, lat],
icon: new AMap.Icon({
image: iconUrl,
size: new AMap.Size(32, 32),
imageSize: new AMap.Size(32, 32)
}),
title: item.name,
offset: new AMap.Pixel(-16, -36),
label: {
content: `<span class='marker-label'>${item.name}</span>`,
direction: 'top',
offset: new AMap.Pixel(0, -16),
clickable: true
}
});
marker.on('mouseover', () => marker.setLabel({ ...marker.getLabel(), content: `<span class='marker-label marker-label-hover'>${item.name}</span>` }));
marker.on('mouseout', () => marker.setLabel({ ...marker.getLabel(), content: `<span class='marker-label'>${item.name}</span>` }));
marker.on('click', () => showInfo(item));
marker.on('labelClick', () => showInfo(item));
marker.setMap(map);
allMarkers.push(marker);
});
}
// 监听筛选项变化
document.querySelectorAll('.filter-panel input[type="checkbox"]').forEach(input => {
input.addEventListener('change', renderMarkers);
});
// 初始化时渲染一次
renderMarkers();
function showInfo(item){
// 关闭已有 infoWindow
if (infoWindow) {
infoWindow.close();
}
// 构建详情内容
const content = `
<div style="min-width:220px;max-width:320px;">
<div style="font-size:18px;font-weight:bold;margin-bottom:6px;">${item.name}</div>
<div style="margin-bottom:4px;">${item.address ? ' ' + item.address : ''}</div>
${item.photo ? `<img src="${item.photo}" style="width:100%;max-height:120px;object-fit:cover;border-radius:6px;margin-bottom:6px;">` : ''}
<div>类型:${item.category || item.type || ''}</div>
<div>推荐指数:${item.recommend_index || ''}</div>
<div>评分:${item.rating || ''}</div>
<div>人均:${item.cost ? '¥' + item.cost : ''}</div>
<div>营业时间:${item.open_time || ''}</div>
<div>电话:${item.phone || ''}</div>
<div style="margin-top:4px;">${item.advantage || ''}</div>
</div>
`;
// 取坐标
const [lng, lat] = item.location.split(',').map(Number);
infoWindow = new AMap.InfoWindow({
content,
offset: new AMap.Pixel(0, -36)
});
infoWindow.open(map, [lng, lat]);
}
// 点击地图空白处关闭详情弹窗
map.on('click', function() {
if (infoWindow) infoWindow.close();
});
</script>
</body>
</html>
“去哪吃”源码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>去哪吃-AI推荐餐厅</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
html, body, #map { height: 100%; margin: 0; padding: 0; }
.amap-info-content { font-size: 14px; }
.legend {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255,255,255,0.9);
border-radius: 6px;
padding: 10px;
box-shadow: 02px 8px rgba(0,0,0,0.15);
z-index: 1000;
}
.legend-item { display: flex; align-items: center; margin-bottom: 6px; }
.legend-icon { width: 20px; height: 20px; margin-right: 6px; }
.marker-label {
font-size: 16px;
color: #fff;
background: rgba(34,34,34,0.75);
border-radius: 6px;
padding: 2px 10px;
box-shadow: 02px 8px rgba(0,0,0,0.15);
white-space: nowrap;
transition: background 0.2s;
border: none;
}
.marker-label-hover {
background: rgba(34,34,34,0.95);
color: #ffe58f;
border: none;
}
/* 强制覆盖高德地图label的蓝色边框和背景 */
.amap-marker-label {
border: none !important;
box-shadow: none !important;
background: none !important;
padding: 0 !important;
}
.filter-panel {
position: absolute;
top: 40px;
right: 20px;
width: 220px;
background: rgba(255,255,255,0.97);
border-radius: 10px;
box-shadow: 02px 16px rgba(0,0,0,0.13);
padding: 18px 18px 12px 18px;
z-index: 1200;
font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
font-size: 15px;
color: #222;
transition: box-shadow 0.2s;
max-height: 600px;
overflow-y: auto;
}
.filter-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 12px;
letter-spacing: 2px;
}
.filter-section { margin-bottom: 14px; }
.filter-label { font-size: 14px; margin-bottom: 6px; color: #666; }
.filter-recommend label, .filter-category label {
display: inline-block;
margin-right: 10px;
margin-bottom: 6px;
cursor: pointer;
font-size: 14px;
}
.filter-category {
max-height: 280px;
overflow-y: auto;
border-radius: 6px;
background: #f7f7f7;
padding: 6px 4px 4px 8px;
border: 1px solid #eee;
}
.filter-category label { width: 120px; }
.filter-panel input[type="checkbox"] {
accent-color: #1890ff;
margin-right: 3px;
vertical-align: middle;
}
.detail-panel {
position: absolute;
top: 40px;
right: 20px;
width: 400px;
background: rgba(255,255,255,0.98);
border-radius: 14px;
box-shadow: 04px 32px rgba(0,0,0,0.13);
padding: 28px 28px 18px 28px;
z-index: 1200;
font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
font-size: 16px;
color: #222;
max-height: 80vh;
overflow-y: auto;
transition: box-shadow 0.2s;
display: none;
}
.detail-title {
font-size: 22px;
font-weight: bold;
margin-bottom: 10px;
color: #1890ff;
letter-spacing: 1px;
}
.detail-recommend {
background: #fffbe6;
color: #faad14;
border-radius: 8px;
padding: 8px 14px;
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
box-shadow: 02px 8px rgba(250,173,20,0.08);
display: flex;
align-items: center;
gap: 8px;
}
.detail-photo {
width: 100%;
max-height: 180px;
object-fit: cover;
border-radius: 8px;
margin-bottom: 10px;
box-shadow: 02px 8px rgba(0,0,0,0.08);
}
.detail-field { margin-bottom: 8px; }
.detail-label { color: #888; margin-right: 6px; }
.detail-analysis {
background: #f6ffed;
color: #389e0d;
border-radius: 8px;
padding: 10px 14px;
margin-top: 12px;
font-size: 15px;
box-shadow: 02px 8px rgba(56,158,13,0.06);
}
.detail-panel-title {
font-size: 20px;
font-weight: bold;
text-align: center;
margin-bottom: 18px;
color: #222;
letter-spacing: 1px;
}
</style>
<!-- 高德地图 JS API -->
<script src="https://webapi.amap.com/maps?v=2.0&key=「你的高德 apikey」"></script>
</head>
<body>
<div id="map"></div>
<div id="detail-panel" class="detail-panel"></div>
<script>
// 直接嵌入餐厅数据
const data = [
{
"name": "杭州市阿里巴巴滨江园区(出发点)",
"type": "出发点",
"location": "120.190371,30.189600",
"address": "杭州市滨江区网商路6号",
"photo": "https://gw.alicdn.com/imgextra/i2/O1CN014DVQlb1oVqsnsrbiT_!!6000000005231-2-tps-64-64.png",
"cost": "-",
"open_time": "-",
"rating": "-",
"phone": "-",
"advantage": "公司园区,交通便利,地铁直达,周边配套齐全。",
"recommend_index": "-",
"category": "出发点",
"is_origin": true
},
{
"name": "海*捞火锅(中南乐游城店)",
"type": "火锅",
"location": "120.190309,30.193994",
"address": "江南大道10号",
"photo": "https://aos-comment.amap.com/B0GU9MT4N7/comment/6845185a8eedb6301996b74240ba088b_2048_2048_80.jpg",
"cost": "80.00",
"open_time": "09:00-07:00",
"rating": "4.7",
"phone": "-",
"advantage": "服务优质,环境舒适,适合聚餐。",
"recommend_index": "5",
"category": "火锅",
"distance_m": 2030,
"duration_s": 459,
"traffic": "驾车约8分钟,公交便利,地铁近。",
"surrounding": "中南乐游城、知嘛家等商场,生活便利。",
"analysis": "距离适中,交通便利,配套丰富,适合团队聚餐。"
},
{
"name": "黄*海鲜大排档(滨江店)",
"type": "海鲜",
"location": "120.195922,30.197039",
"address": "浙商发展大厦*座1-2楼(地铁站G口)",
"photo": "https://aos-comment.amap.com/B0J1B5R0EY/comment/0e8ed310fa3bb3135ddbefe27244562f_2048_2048_80.jpg",
"cost": "139.00",
"open_time": "11:00-15:30 17:30-02:30",
"rating": "4.7",
"phone": "-",
"advantage": "菜品新鲜,适合多人聚餐,夜宵丰富。",
"recommend_index": "5",
"category": "海鲜",
"distance_m": 2395,
"duration_s": 478,
"traffic": "驾车约8分钟,地铁直达,公交便利。",
"surrounding": "知嘛家等商场,夜生活丰富。",
"analysis": "交通便利,夜宵丰富,适合下班聚会。"
},
{
"name": "群*饭店(信诚路店)",
"type": "中餐",
"location": "120.174707,30.183901",
"address": "滨安路11号(近信诚路)",
"photo": "http://store.is.autonavi.com/showpic/2ac9e95efffe6e27a8eda43ef8271134",
"cost": "66.00",
"open_time": "11:00-13:30 16:00-20:30",
"rating": "4.7",
"phone": "-",
"advantage": "家常菜,性价比高,适合日常用餐。",
"recommend_index": "4",
"category": "中餐",
"distance_m": 2797,
"duration_s": 757,
"traffic": "驾车约13分钟,公交可达。",
"surrounding": "周边无大型商场,环境安静。",
"analysis": "性价比高,适合小团队日常聚餐。"
},
{
"name": "龙*·滨江烤全羊(杭州总店)",
"type": "烤肉",
"location": "120.195890,30.184231",
"address": "长河路47*号和瑞科技广场S*幢楼下",
"photo": "http://store.is.autonavi.com/showpic/45d72ae00c69cf56d4fe514b1346c55c",
"cost": "132.00",
"open_time": "11:00-21:00",
"rating": "4.8",
"phone": "-",
"advantage": "烤全羊特色,适合聚会,环境宽敞。",
"recommend_index": "5",
"category": "烤肉",
"distance_m": 1226,
"duration_s": 254,
"traffic": "驾车约4分钟,公交可达。",
"surrounding": "保利·时光里等商场,配套齐全。",
"analysis": "距离最近,评分最高,配套好,强烈推荐。"
},
{
"name": "东*方(杭州保利时光里店)",
"type": "中餐",
"location": "120.197088,30.182094",
"address": "长河街道长河路与滨康路交叉口保利时光里*楼13号楼2017商铺",
"photo": "http://store.is.autonavi.com/query_pic?id=st1ef630c8-39ef-4d63-93ce-ba3a9732851e&user=search&operate=original",
"cost": "-",
"open_time": "-",
"rating": "4.8",
"phone": "-",
"advantage": "环境优雅,适合家庭聚餐。",
"recommend_index": "5",
"category": "中餐",
"distance_m": 2088,
"duration_s": 491,
"traffic": "驾车约8分钟,公交便利。",
"surrounding": "保利·时光里等商场,生活便利。",
"analysis": "评分高,环境好,适合家庭和小型聚会。"
}
];
// 图标映射表
const typeIcons = {
'辣椒': '「需要自己准备」',
'甜点': '「需要自己准备」',
'咖啡': '「需要自己准备」',
'中餐': '「需要自己准备」',
'日料': '「需要自己准备」',
'快餐': '「需要自己准备」',
'烤肉': '「需要自己准备」',
'烧烤': '「需要自己准备」',
'星星': '「需要自己准备」',
'火锅': '「需要自己准备」',
'牛排': '「需要自己准备」',
'面食': '「需要自己准备」',
'海鲜': '「需要自己准备」',
'韩餐': '「需要自己准备」',
'小吃': '「需要自己准备」',
'粤菜': '「需要自己准备」',
'default': '「需要自己准备」'
};
function getCategoryIcon(category){
if (!category) return typeIcons['default'];
if (category === '出发点') return typeIcons['星星'];
if (category === '中餐' || category === '杭帮菜/浙江菜' || category === '湘菜' || category === '东北菜' || category === '素食' || category === '私房菜') return typeIcons['中餐'];
if (category === '粤菜') return typeIcons['粤菜'];
if (category === '川菜') return typeIcons['辣椒'];
if (category === '火锅') return typeIcons['火锅'];
if (category === '烧烤/烤肉' || category === '烤肉') return typeIcons['烤肉'];
if (category === '烧烤') return typeIcons['烧烤'];
if (category === '日料') return typeIcons['日料'];
if (category === '韩餐') return typeIcons['韩餐'];
if (category === '西餐') return typeIcons['牛排'];
if (category === '面馆') return typeIcons['面食'];
if (category === '小吃') return typeIcons['小吃'];
if (category === '甜品/饮品') return typeIcons['甜点'];
if (category === '咖啡馆') return typeIcons['咖啡'];
if (category === '快餐') return typeIcons['快餐'];
if (category === '海鲜') return typeIcons['海鲜'];
return typeIcons['default'];
}
// 初始化地图
constmap = new AMap.Map('map', {
center: [120.190371, 30.189600],
zoom: 14
});
let allMarkers = [];
let allLines = [];
function renderMarkers(){
// 清除旧marker和线
allMarkers.forEach(m => m.setMap(null));
allMarkers = [];
allLines.forEach(l => l.setMap(null));
allLines = [];
if (!data.length) return;
// 出发点
const origin = data.find(d => d.is_origin);
// 餐厅
const restaurants = data.filter(d => !d.is_origin);
// 出发点marker
if (origin) {
const [lng, lat] = origin.location.split(',').map(Number);
const marker = new AMap.Marker({
position: [lng, lat],
icon: new AMap.Icon({
image: getCategoryIcon('出发点'),
size: new AMap.Size(40, 40),
imageSize: new AMap.Size(40, 40)
}),
title: origin.name,
offset: new AMap.Pixel(-20, -40),
label: {
content: `<span class='marker-label'>${origin.name}</span>`,
direction: 'top',
offset: new AMap.Pixel(0, -20),
clickable: true
}
});
marker.setMap(map);
allMarkers.push(marker);
}
// 餐厅marker和连线
restaurants.forEach(item => {
if (!item.location) return;
const [lng, lat] = item.location.split(',').map(Number);
const iconUrl = getCategoryIcon(item.category);
const marker = new AMap.Marker({
position: [lng, lat],
icon: new AMap.Icon({
image: iconUrl,
size: new AMap.Size(32, 32),
imageSize: new AMap.Size(32, 32)
}),
title: item.name,
offset: new AMap.Pixel(-16, -36),
label: {
content: `<span class='marker-label'>${item.name}</span>`,
direction: 'top',
offset: new AMap.Pixel(0, -16),
clickable: true
}
});
marker.on('mouseover', () => marker.setLabel({ ...marker.getLabel(), content: `<span class='marker-label marker-label-hover'>${item.name}</span>` }));
marker.on('mouseout', () => marker.setLabel({ ...marker.getLabel(), content: `<span class='marker-label'>${item.name}</span>` }));
marker.on('click', () => showInfo(item));
marker.on('labelClick', () => showInfo(item));
marker.setMap(map);
allMarkers.push(marker);
// 连线
if (origin && origin.location) {
const [olng, olat] = origin.location.split(',').map(Number);
const line = new AMap.Polyline({
path: [ [olng, olat], [lng, lat] ],
strokeColor: '#1890ff',
strokeWeight: 4,
strokeOpacity: 0.7,
isOutline: true,
outlineColor: '#fff'
});
line.setMap(map);
allLines.push(line);
}
});
}
// 智能推荐(评分最高且距离最近)
function showRecommend(){
if (!data.length) return;
const restaurants = data.filter(d => !d.is_origin);
// 评分优先,距离次之
let best = restaurants[0];
restaurants.forEach(r => {
if (
Number(r.rating) > Number(best.rating) ||
(r.rating === best.rating && r.distance_m < best.distance_m)
) {
best = r;
}
});
showInfo(best, true);
}
// 右侧详情面板
function showInfo(item, isRecommend = false){
const panel = document.getElementById('detail-panel');
if (!item) { panel.style.display = 'none'; return; }
let html = '';
html += `<div class='detail-panel-title'>AI推荐餐厅</div>`;
if (isRecommend) {
html += `<div class='detail-recommend'> 智能推荐:<span>${item.name}</span></div>`;
}
html += `<div class='detail-title'>${item.name}</div>`;
if (item.photo) html += `<img class='detail-photo' src='${item.photo}' alt='${item.name}'>`;
html += `<div class='detail-field'><span class='detail-label'>类型:</span>${item.type || ''}</div>`;
html += `<div class='detail-field'><span class='detail-label'>地址:</span>${item.address || ''}</div>`;
if (item.distance_m) html += `<div class='detail-field'><span class='detail-label'>距离:</span>${(item.distance_m/1000).toFixed(2)} km</div>`;
if (item.duration_s) html += `<div class='detail-field'><span class='detail-label'>车程:</span>${Math.round(item.duration_s/60)} 分钟</div>`;
if (item.cost && item.cost !== '-') html += `<div class='detail-field'><span class='detail-label'>人均:</span>¥${item.cost}</div>`;
if (item.open_time && item.open_time !== '-') html += `<div class='detail-field'><span class='detail-label'>营业时间:</span>${item.open_time}</div>`;
if (item.rating && item.rating !== '-') html += `<div class='detail-field'><span class='detail-label'>评分:</span>${item.rating}</div>`;
if (item.phone && item.phone !== '-') html += `<div class='detail-field'><span class='detail-label'>电话:</span>${item.phone}</div>`;
if (item.traffic) html += `<div class='detail-field'><span class='detail-label'>交通:</span>${item.traffic}</div>`;
if (item.surrounding) html += `<div class='detail-field'><span class='detail-label'>周边配套:</span>${item.surrounding}</div>`;
if (item.advantage) html += `<div class='detail-field'><span class='detail-label'>优点:</span>${item.advantage}</div>`;
if (item.analysis) html += `<div class='detail-analysis'>${item.analysis}</div>`;
panel.innerHTML = html;
panel.style.display = 'block';
}
// 地图点击空白关闭详情
map.on('click', function() {
document.getElementById('detail-panel').style.display = 'none';
});
renderMarkers();
showRecommend();
</script>
</body>
</html>
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/138140.html