Vue3对接高德地图POI搜索

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>

 效果展示

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值