1.第一步
申请高德地图API,参考视频
准备-如何申请一个免费的高德地图key?_哔哩哔哩_bilibili
2.第二步
创建vue3项目
【带小白做毕设】01. 前端Vue3 框架的快速搭建以及项目工程的讲解_哔哩哔哩_bilibili
3.第三步
在index.html中添加高德地图API密钥及服务
<!--高德地图API密钥-用于渲染旅游线路的地图-->
<script>
window._AMapSecurityConfig = {
securityJsCode: "你的密钥",
};
</script>
<script src="https://webapi.amap.com/maps?v=2.0&key=fe04d3154c17c9f3439d51118fbec8ae&plugin=AMap.Geocoder,AMap.PlaceSearch"></script>
4.第四步
编写前端代码,该功能采用高德地图API,运用了输入提示和POI搜索插件,在输入地点及查询的关键字时进行提示,用户点击搜索后会对输入框进行非空检查,有内容调用placeSearch.search()方法发起搜索请求,然后对搜索结果进行标记marker,创建信息窗体和分页显示。里面先默认展示几个结果,使页面看起来美观。
<template>
<!-- 地图容器 -->
<div id="map_container">
<!-- 左侧内容区域 -->
<div class="left-panel">
<!-- 搜索结果列表和分页 -->
<div id="custom-panel" class="custom-list">
<div class="default-header">
<h3>热门景点推荐</h3>
<p>输入城市和关键词开始搜索</p>
<!-- 搜索输入框 -->
<div class="search-input">
<input
type="text"
id="city_input"
v-model="cityInput"
placeholder="请先输入地名:"
@keyup.enter="executeSearch"
>
<input
type="text"
id="search_input"
v-model="searchInput"
placeholder="请再输入关键字:"
@keyup.enter="executeSearch"
>
<button @click="executeSearch">搜索</button>
</div>
</div>
<!-- 默认内容(未搜索时显示) -->
<div v-if="!searchExecuted" class="default-content">
<div v-for="poi in defaultPlaces" :key="poi.id" class="result-item" @click="setCenterOnDefaultPoi(poi)">
<div class="item-content">
<div class="item-image">
<img :src="poi.photos[0].url" :alt="poi.name" class="single-image" />
</div>
<div class="item-details">
<a :href="'https://www.mafengwo.cn/search/q.php?q=' + encodeURIComponent(poi.cityname) + encodeURIComponent(poi.name)" target="_blank">
<strong>{{ poi.name }}</strong>
</a>
<div class="web">
<a :href="`https://${poi.website}`" target="_blank" rel="noopener noreferrer">{{ poi.website}}</a>
</div>
<div>地址:{{poi.pname}}{{poi.cityname}}{{poi.adname}}{{ poi.address }}</div>
<div>类型:{{poi.type}}</div>
<div>电话:{{ poi.tel || '无电话' }}</div>
</div>
</div>
</div>
</div>
<!-- 搜索结果列表 -->
<div v-for="poi in pois" :key="poi.id" class="result-item" @click="setCenterOnPoi(poi)">
<div class="item-content">
<!--轮播图样式-->
<div class="item-image">
<el-carousel
v-if="poi.photos && poi.photos.length > 0"
:interval="5000"
height="160px"
indicator-position="outside"
>
<el-carousel-item v-for="(photo, index) in poi.photos" :key="index">
<img :src="photo.url || defaultImage" :alt="poi.name" class="carousel-image" />
</el-carousel-item>
</el-carousel>
<img v-else :src="defaultImage" :alt="poi.name" class="single-image" />
</div>
<div class="item-details">
<a :href="'https://www.mafengwo.cn/search/q.php?q=' + encodeURIComponent(poi.cityname) + encodeURIComponent(poi.name)" target="_blank">
<strong>{{ poi.name }}</strong>
</a>
<div class="web">
<a :href="`https://${poi.website}`" target="_blank" rel="noopener noreferrer">{{ poi.website}}</a>
</div>
<div>地址:{{poi.pname}}{{poi.cityname}}{{poi.adname}}{{ poi.address }}</div>
<div>类型:{{poi.type}}</div>
<div>电话:{{ poi.tel || '无电话' }}</div>
</div>
</div>
</div>
<!-- 分页栏 -->
<div class="pagination" v-if="totalPages > 0 && pois.length > 0">
<button @click="changePage(currentPage - 1)" :disabled="currentPage === 1">上一页</button>
<span>第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</span>
<button @click="changePage(currentPage + 1)" :disabled="currentPage === totalPages">下一页</button>
</div>
<div v-if="searchExecuted && pois.length === 0" class="no-results">
没有找到相关结果
</div>
</div>
</div>
<!-- 右侧地图区域 -->
<div class="right-panel">
<!-- 地图显示区域 -->
<div id="container"></div>
<!-- 地图控件开关 -->
<div class='input-card'>
<div class="input-item">
<input type="checkbox" @click="toggleScale($event.target)" checked/> 比例尺
</div>
<div class="input-item">
<input type="checkbox" @click="toggleToolBar($event.target)" checked/> 工具条
</div>
<div class="input-item">
<input type="checkbox" @click="toggleGeolocation($event.target)" checked/> 定位控件
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from "vue";
import {ElMessage} from "element-plus";
// 地图相关变量声明
let map: AMap.Map | null = null;
let markers: AMap.Marker[] = [];
let toolbar: AMap.ToolBar | null = null;
let scale: AMap.Scale | null = null;
let geolocation: AMap.Geolocation | null = null;
// 响应式数据
const cityInput = ref('');
const searchInput = ref('');
const pois = ref<any[]>([]);
const currentPage = ref(1);
const totalPages = ref(0);
const pageSize = ref(6);
const searchExecuted = ref(false);
const defaultImage = 'https://img.js.design/assets/compImg/JoR2el00cacd6ed83a3fe7e63b88afd022cbb5.png';
// 在响应式数据部分添加
const defaultPlaces = ref([
{
id: 'default1',
name: '北京故宫',
cityname: '北京市',
pname: '北京市',
adname: '东城区',
address: '景山前街4号',
type: '名胜古迹',
tel: '010-85007421',
website: 'www.dpm.org.cn',
photos: [
{ url: 'https://images.unsplash.com/photo-1547981609-4b6bfe67ca0b?w=400&h=300' }
]
},
{
id: 'default2',
name: '上海外滩',
cityname: '上海市',
pname: '上海市',
adname: '黄浦区',
address: '中山东一路',
type: '城市景观',
tel: '021-12345678',
website: 'www.shanghai.gov.cn',
photos: [
{ url: 'https://dimg04.c-ctrip.com/images/0106h120008srd15c6500_R_228_10000.jpg' }
]
},
{
id: 'default3',
name: '广州塔',
cityname: '广州市',
pname: '广东省',
adname: '海珠区',
address: '阅江西路222号',
type: '地标建筑',
tel: '020-89338222',
website: 'www.cantontower.com',
photos: [
{ url: 'https://www.cantontower.com/Uploads/ueditor/image/20190619/6369656758462028578901253.jpg' }
]
},
{
id: 'default4',
name: '成都大熊猫繁育研究基地',
cityname: '成都市',
pname: '四川省',
adname: '成华区',
address: '熊猫大道1375号',
type: '动物园',
tel: '028-83510033',
website: 'www.panda.org.cn',
photos: [
{ url: 'https://p1-q.mafengwo.net/s9/M00/99/04/wKgBs1fYx3qADOEbAAtEDC3KXNo01.jpeg?imageMogr2%2Fthumbnail%2F%21305x183r%2Fgravity%2FCenter%2Fcrop%2F%21305x183%2Fquality%2F100' }
]
},
{
id: 'default5',
name: '西安兵马俑',
cityname: '西安市',
pname: '陕西省',
adname: '临潼区',
address: '秦陵北路',
type: '历史遗址',
tel: '029-81399001',
website: 'www.bmy.com.cn',
photos: [
{ url: 'https://sales.mafengwo.net/mfs/s17/M00/89/6A/CoUBXl-7NmWALIABABF73vKZaXI08.jpeg?imageMogr2%2Fthumbnail%2F%21440x260r%2Fgravity%2FCenter%2Fcrop%2F%21440x260%2Fquality%2F100' }
]
},
{
id: 'default6',
name: '杭州西湖',
cityname: '杭州市',
pname: '浙江省',
adname: '西湖区',
address: '西湖风景名胜区',
type: '自然景观',
tel: '0571-87179617',
website: 'westlake.hangzhou.gov.cn',
photos: [
{ url: 'https://p1-q.mafengwo.net/s13/M00/DA/7D/wKgEaVysAG-AXtKuAASdij17Z_A50.jpeg?imageMogr2%2Fthumbnail%2F%21296x156r%2Fgravity%2FCenter%2Fcrop%2F%21296x156%2Fquality%2F100' }
]
},
]);
// 组件挂载时初始化地图
onMounted(() => {
if (window.AMap) {
initMap();
} else {
console.error("AMap未加载");
// 可以在这里添加AMap的CDN动态加载逻辑
}
});
// 定位到默认POI
const setCenterOnDefaultPoi = (poi: any) => {
// 这里可以使用一些默认坐标
const defaultCoords: Record<string, [number, number]> = {
'default1': [116.397, 39.916], // 北京故宫
'default2': [121.490, 31.239], // 上海外滩
'default3': [113.324, 23.106], // 广州塔
'default4': [104.147, 30.741], // 成都大熊猫基地
'default5': [109.277, 34.385], // 西安兵马俑
'default6': [120.155, 30.274] // 杭州西湖
};
if (map && defaultCoords[poi.id]) {
map.setCenter(defaultCoords[poi.id]);
map.setZoom(16);
}
};
/**
* 初始化地图
*/
const initMap = () => {
map = new AMap.Map("container", {
viewMode: "2D",
zoom: 15,
center: [116.397428, 39.90923],
resizeEnable: true,
});
AMap.plugin([
"AMap.ToolBar",
"AMap.Scale",
"AMap.Geolocation",
"AMap.AutoComplete",
"AMap.PlaceSearch"
], () => {
initControls();
initSearch();
});
};
/**
* 初始化地图控件
*/
const initControls = () => {
if (!map) return;
toolbar = new AMap.ToolBar({
visible: true,
position: { bottom: '80px', right: '20px' },
});
map.addControl(toolbar);
scale = new AMap.Scale({
visible: true,
position: { bottom: '40px', right: '15px' },
});
map.addControl(scale);
geolocation = new AMap.Geolocation({
visible: true,
enableHighAccuracy: true,
timeout: 10000,
position: { bottom: '150px', right: '20px' },
zoomToAccuracy: true,
});
map.addControl(geolocation);
};
/**
* 初始化搜索功能
*/
const initSearch = () => {
if (!map) return;
// 城市输入自动完成
const autocityInput = new AMap.AutoComplete({ city: "", input: "city_input" });
autocityInput.on("select", (e) => {
cityInput.value = e.poi.name;
if (searchInput.value) {
executeSearch();
}
});
// 关键字输入自动完成
const autocomplete = new AMap.AutoComplete({ city: "", input: "search_input" });
autocomplete.on("select", (e) => {
searchInput.value = e.poi.name;
executeSearch();
});
};
/**
* 执行搜索
*/
const executeSearch = () => {
if (!searchInput.value){
ElMessage.error('您还没有输入搜索内容!');
return;
}
searchExecuted.value = true;
currentPage.value = 1; // 重置为第一页
const placeSearch = new AMap.PlaceSearch({
pageSize: pageSize.value,
city: cityInput.value,
citylimit: true,
map: map,
autoFitView: true,
pageIndex: currentPage.value
});
placeSearch.search(searchInput.value, (status, result) => {
console.log('搜索结果:', { status, result });
if (status === 'complete' && result.info === 'OK') {
pois.value = result.poiList.pois;
totalPages.value = Math.max(1, Math.ceil(result.poiList.count / pageSize.value));
clearMarkers();
createMarkers(result.poiList.pois);
} else {
pois.value = [];
totalPages.value = 0;
}
});
};
/**
* 切换页码
*/
const changePage = (page: number) => {
if (page < 1 || page > totalPages.value) return;
currentPage.value = page;
const placeSearch = new AMap.PlaceSearch({
pageSize: pageSize.value,
city: cityInput.value,
citylimit: true,
map: map,
autoFitView: true,
pageIndex: currentPage.value
});
placeSearch.search(searchInput.value, (status, result) => {
if (status === 'complete' && result.info === 'OK') {
pois.value = result.poiList.pois;
totalPages.value = Math.max(1, Math.ceil(result.poiList.count / pageSize.value));
clearMarkers();
createMarkers(result.poiList.pois);
// 滚动到列表顶部
const panel = document.getElementById('custom-panel');
if (panel) panel.scrollTop = 0;
}
});
};
/**
* 创建标记点
*/
const createMarkers = (pois: any[]) => {
if (!map) return;
pois.forEach(poi => {
if (!poi.location) return;
const marker = new AMap.Marker({
position: poi.location,
title: poi.name,
extData: { poi }
});
const infoWindow = createInfoWindow(poi);
marker.setExtData({ ...marker.getExtData(), infoWindow });
marker.setMap(map);
markers.push(marker);
marker.on('click', () => {
map?.clearInfoWindow();
infoWindow.open(map!, marker.getPosition()!);
});
});
};
/**
* 创建信息窗体
*/
const createInfoWindow = (poi: any) => {
return new AMap.InfoWindow({
content: `
<div class="custom-info-window">
<h3>${poi.name}</h3>
<div class="info-content">
<p><i class="icon-address"></i>${poi.address || '无地址信息'}</p>
${poi.tel ? `<p><i class="icon-tel"></i> ${poi.tel}</p>` : ''}
</div>
</div>
`,
offset: new AMap.Pixel(0, -30),
size: new AMap.Size(250, 'auto'),
border: 'none',
closeWhenClickMap: true
});
};
/**
* 清除所有标记点
*/
const clearMarkers = () => {
if (!map) return;
markers.forEach(marker => map.remove(marker));
markers = [];
};
/**
* 定位到指定POI
*/
const setCenterOnPoi = (poi: any) => {
if (!poi.location || !map) return;
const { lng, lat } = poi.location;
map.setCenter([lng, lat]);
map.setZoom(17);
const marker = markers.find(m => {
const pos = m.getPosition();
return pos && pos.lng === lng && pos.lat === lat;
});
if (marker) {
const extData = marker.getExtData();
if (extData.infoWindow) {
map.clearInfoWindow();
extData.infoWindow.open(map, marker.getPosition()!);
}
}
};
/**
* 切换控件显示状态
*/
const toggleToolBar = (checkbox: EventTarget) => {
const input = checkbox as HTMLInputElement;
input.checked ? toolbar?.show() : toolbar?.hide();
};
const toggleScale = (checkbox: EventTarget) => {
const input = checkbox as HTMLInputElement;
input.checked ? scale?.show() : scale?.hide();
};
const toggleGeolocation = (checkbox: EventTarget) => {
const input = checkbox as HTMLInputElement;
input.checked ? geolocation?.show() : geolocation?.hide();
};
// 组件卸载时清理
onUnmounted(() => {
map?.destroy();
});
</script>
<style scoped>
/* 基础布局 */
#map_container {
display: flex;
width: 100%;
height: 665px;
margin: 0;
padding: 0;
overflow: hidden;
}
/* 左侧面板 */
.left-panel {
width: 50%;
height: 100%;
margin-bottom: 100px;
background: #f5f5f5;
overflow-y: auto;
display: flex;
flex-direction: column;
}
/* 右侧面板 */
.right-panel {
flex: 1;
position: relative;
display: flex;
justify-content: center;
margin: 30px 30px;
align-items: center;
background: #eaeaea;
}
/* 地图容器 */
#container {
width: 100%;
height: 100%;
z-index: 1;
}
/* 搜索框样式 */
.search-input {
display: flex;
gap: 10px;
margin: 20px ;
}
.search-input input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
flex: 1;
}
.search-input button {
padding: 8px 16px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
}
.search-input button:hover {
background: #45a049;
}
/* 默认内容样式 */
.default-content {
padding: 15px;
}
.default-header {
text-align: center;
margin-bottom: 20px;
padding: 10px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 8px;
}
.default-header h3 {
color: #2c3e50;
margin-bottom: 5px;
}
.default-header p {
color: #7f8c8d;
font-size: 14px;
}
/* 搜索结果面板 */
.custom-list {
flex: 1;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow-y: scroll; /* 修改为scroll确保滚动功能 */
display: flex;
flex-direction: column;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 和 Edge */
}
.custom-list::-webkit-scrollbar {
display: none; /* Chrome, Safari 和 Opera */
}
/* 单个结果项 */
.result-item {
padding: 12px 15px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background 0.5s;
}
.result-item:hover {
background: #f9f9f9;
}
.item-content {
display: flex;
gap: 12px;
}
/* 轮播图样式 */
.item-image {
width: 240px;
height: 160px;
flex-shrink: 0;
background: #f5f5f5;
border-radius: 4px;
overflow: hidden;
}
.carousel-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.single-image {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 调整轮播指示器样式 */
:deep(.el-carousel__indicators) {
bottom: -25px;
}
:deep(.el-carousel__button) {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #c0c4cc;
}
.item-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.item-details {
flex: 1;
min-width: 0;
}
/*名称链接*/
.item-details a {
font-size: 18px;
font-weight: 500;
color: #1a73e8;
text-decoration: none;
display: block;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-content:hover .item-details a{
color: #189500;
text-decoration: underline;
}
/*网站*/
.item-details .web a {
}
.item-content:hover .item-details .web a{
color: #FF9E01;
text-decoration: underline;
}
.item-details a:hover {
text-decoration: underline;
}
.item-details div {
font-size: 14px;
color: #666;
line-height: 1.4;
margin: 2px 0;
}
/* 分页样式 */
.pagination {
padding: 15px;
background: #f9f9f9;
border-top: 1px solid #eee;
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
flex-wrap: wrap;
position: sticky;
bottom: 0;
}
.pagination button {
min-width: 80px;
padding: 6px 12px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.pagination button:hover:not(:disabled) {
background: #3d8b40;
}
.pagination button:disabled {
background: #cccccc;
cursor: not-allowed;
}
.pagination span {
font-size: 14px;
color: #666;
}
/* 无结果提示 */
.no-results {
padding: 20px;
text-align: center;
color: #666;
}
/* 控件开关面板 */
.input-card {
position: absolute;
top: 20px;
right: 10px;
z-index: 10;
background: white;
padding: 10px;
border-radius: 4px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.input-item {
display: flex;
align-items: center;
gap: 6px;
margin: 8px 0;
font-size: 14px;
color: #666;
}
/* 信息窗口样式 */
:deep(.custom-info-window) {
padding: 12px;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
:deep(.custom-info-window h3) {
margin: 0 0 8px 0;
font-size: 16px;
color: #1a73e8;
padding-bottom: 6px;
border-bottom: 1px solid #f0f0f0;
}
:deep(.custom-info-window .info-content) {
font-size: 14px;
}
:deep(.custom-info-window p) {
margin: 6px 0;
display: flex;
align-items: center;
}
</style>
效果展示