/** * 会面点 - Meeting Point * 前端应用主逻辑 */ // ======================================== // 全局状态 // ======================================== const state = { map: null, amapKey: '', // Web服务API Key amapJsKey: '', // JS API Key amapJsSecret: '', // JS API 安全密钥 locations: [], // 参与者位置列表 markers: [], // 位置标记 centerMarker: null, // 中心点标记 poiMarkers: [], // POI标记 center: null, // 计算出的中心点 searchCircle: null, // 搜索范围圆 infoWindow: null, // 信息窗口 currentPOIs: [], // 当前搜索结果 colors: [ '#3b82f6', '#ef4444', '#10b981', '#8b5cf6', '#f59e0b', '#ec4899', '#06b6d4', '#84cc16' ] }; // ======================================== // 初始化 // ======================================== document.addEventListener('DOMContentLoaded', async () => { await loadConfig(); initMap(); bindEvents(); }); async function loadConfig() { try { const response = await fetch('/api/config'); const data = await response.json(); state.amapKey = data.amap_key; state.amapJsKey = data.amap_js_key || data.amap_key; state.amapJsSecret = data.amap_js_secret; // 配置安全密钥(必须在加载JS API之前设置) if (state.amapJsSecret) { window._AMapSecurityConfig = { securityJsCode: state.amapJsSecret }; } // 动态加载高德地图JS API return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = `https://webapi.amap.com/maps?v=2.0&key=${state.amapJsKey}&plugin=AMap.PlaceSearch,AMap.Geocoder,AMap.AutoComplete`; script.onload = resolve; script.onerror = () => reject(new Error('加载高德地图JS API失败')); document.head.appendChild(script); }); } catch (error) { console.error('加载配置失败:', error); alert('加载配置失败,请检查后端服务是否启动'); } } function initMap() { // 初始化地图 state.map = new AMap.Map('mapContainer', { zoom: 12, center: [116.397428, 39.90923], // 默认北京 mapStyle: 'amap://styles/dark', // 深色主题 viewMode: '2D' }); // 初始化信息窗口 state.infoWindow = new AMap.InfoWindow({ isCustom: true, autoMove: true, offset: new AMap.Pixel(0, -40) }); // 地图点击事件 state.map.on('click', handleMapClick); // 尝试定位到用户位置 AMap.plugin('AMap.Geolocation', () => { const geolocation = new AMap.Geolocation({ enableHighAccuracy: true, timeout: 10000 }); geolocation.getCurrentPosition((status, result) => { if (status === 'complete') { state.map.setCenter([result.position.lng, result.position.lat]); } }); }); } // ======================================== // 事件绑定 // ======================================== function bindEvents() { // 搜索输入 const searchInput = document.getElementById('searchInput'); const clearSearch = document.getElementById('clearSearch'); const searchTips = document.getElementById('searchTips'); let searchTimeout; searchInput.addEventListener('input', (e) => { const value = e.target.value.trim(); clearSearch.style.display = value ? 'block' : 'none'; clearTimeout(searchTimeout); if (value.length >= 2) { searchTimeout = setTimeout(() => fetchSearchTips(value), 300); } else { searchTips.classList.remove('active'); } }); clearSearch.addEventListener('click', () => { searchInput.value = ''; clearSearch.style.display = 'none'; searchTips.classList.remove('active'); }); // 点击外部关闭搜索提示 document.addEventListener('click', (e) => { if (!e.target.closest('.search-box')) { searchTips.classList.remove('active'); } }); // 快捷标签 document.querySelectorAll('.tag').forEach(tag => { tag.addEventListener('click', () => { document.querySelectorAll('.tag').forEach(t => t.classList.remove('active')); tag.classList.add('active'); document.getElementById('poiKeywords').value = tag.dataset.keyword; }); }); // 搜索半径滑块 const radiusSlider = document.getElementById('searchRadius'); const radiusValue = document.getElementById('radiusValue'); radiusSlider.addEventListener('input', () => { const value = parseInt(radiusSlider.value); radiusValue.textContent = value >= 1000 ? `${value / 1000} 公里` : `${value} 米`; }); // 搜索按钮 document.getElementById('searchBtn').addEventListener('click', handleSearch); // POI关键词输入 document.getElementById('poiKeywords').addEventListener('input', () => { document.querySelectorAll('.tag').forEach(t => t.classList.remove('active')); }); } // ======================================== // 搜索提示 // ======================================== async function fetchSearchTips(keywords) { const searchTips = document.getElementById('searchTips'); try { const response = await fetch(`/api/tips?keywords=${encodeURIComponent(keywords)}&city=`); const data = await response.json(); if (data.tips && data.tips.length > 0) { renderSearchTips(data.tips); } else { searchTips.classList.remove('active'); } } catch (error) { console.error('获取搜索提示失败:', error); } } function renderSearchTips(tips) { const searchTips = document.getElementById('searchTips'); searchTips.innerHTML = tips.map(tip => `
${tip.name}
${tip.district}${tip.address}
`).join(''); // 绑定点击事件 searchTips.querySelectorAll('.search-tip-item').forEach(item => { item.addEventListener('click', () => { const location = { name: item.dataset.name, address: item.dataset.address, coordinate: { lng: parseFloat(item.dataset.lng), lat: parseFloat(item.dataset.lat) } }; addLocation(location); // 清理搜索框 document.getElementById('searchInput').value = ''; document.getElementById('clearSearch').style.display = 'none'; searchTips.classList.remove('active'); }); }); searchTips.classList.add('active'); } // ======================================== // 地图点击添加位置 // ======================================== function handleMapClick(e) { const lnglat = e.lnglat; // 逆地理编码获取地址 AMap.plugin('AMap.Geocoder', () => { const geocoder = new AMap.Geocoder(); geocoder.getAddress([lnglat.lng, lnglat.lat], (status, result) => { if (status === 'complete' && result.info === 'OK') { const address = result.regeocode.formattedAddress; const location = { name: `位置 ${state.locations.length + 1}`, address: address, coordinate: { lng: lnglat.lng, lat: lnglat.lat } }; addLocation(location); } }); }); } // ======================================== // 位置管理 // ======================================== function addLocation(location) { state.locations.push(location); // 添加地图标记 const index = state.locations.length - 1; const color = state.colors[index % state.colors.length]; const marker = new AMap.Marker({ position: [location.coordinate.lng, location.coordinate.lat], content: `
${index + 1}
`, offset: new AMap.Pixel(-18, -18) }); marker.on('click', () => { showInfoWindow(marker, location); }); state.markers.push(marker); state.map.add(marker); // 调整视野 if (state.locations.length > 1) { state.map.setFitView(state.markers); } else { state.map.setCenter([location.coordinate.lng, location.coordinate.lat]); state.map.setZoom(14); } // 更新UI updateLocationList(); updateSearchButton(); // 清除之前的搜索结果 clearSearchResults(); } function removeLocation(index) { // 移除标记 state.map.remove(state.markers[index]); state.markers.splice(index, 1); state.locations.splice(index, 1); // 更新剩余标记的编号和颜色 state.markers.forEach((marker, i) => { const color = state.colors[i % state.colors.length]; marker.setContent(`
${i + 1}
`); }); // 更新UI updateLocationList(); updateSearchButton(); // 调整视野 if (state.markers.length > 0) { state.map.setFitView(state.markers); } // 清除之前的搜索结果 clearSearchResults(); } function updateLocationList() { const listEl = document.getElementById('locationList'); const countEl = document.getElementById('locationCount'); countEl.textContent = state.locations.length; if (state.locations.length === 0) { listEl.innerHTML = `
  • 🗺️ 还没有添加位置 搜索地址或点击地图开始
  • `; return; } listEl.innerHTML = state.locations.map((loc, index) => { const color = state.colors[index % state.colors.length]; return `
  • ${index + 1}
    ${loc.name}
    ${loc.address || '暂无地址'}
  • `; }).join(''); } function updateSearchButton() { const btn = document.getElementById('searchBtn'); btn.disabled = state.locations.length < 2; } // ======================================== // 搜索功能 // ======================================== async function handleSearch() { const keywords = document.getElementById('poiKeywords').value.trim(); const radius = parseInt(document.getElementById('searchRadius').value); if (!keywords) { alert('请输入搜索关键词'); return; } if (state.locations.length < 2) { alert('请至少添加2个位置'); return; } showLoading(true); try { // 1. 计算中心点 const centerResponse = await fetch('/api/center', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ locations: state.locations }) }); const centerData = await centerResponse.json(); state.center = centerData.center; // 显示中心点标记 showCenterMarker(); // 2. 搜索周边POI const searchResponse = await fetch('/api/search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ center: state.center, keywords: keywords, radius: radius }) }); const searchData = await searchResponse.json(); // 显示搜索结果 showSearchResults(searchData.pois, radius); } catch (error) { console.error('搜索失败:', error); alert('搜索失败,请稍后重试'); } finally { showLoading(false); } } function showCenterMarker() { // 移除旧的中心点标记 if (state.centerMarker) { state.map.remove(state.centerMarker); } // 创建新的中心点标记 state.centerMarker = new AMap.Marker({ position: [state.center.lng, state.center.lat], content: '
    ', offset: new AMap.Pixel(-24, -24), zIndex: 200 }); state.map.add(state.centerMarker); // 显示中心点信息 document.getElementById('centerInfo').style.display = 'block'; } function showSearchResults(pois, radius) { // 清除旧的POI标记 clearPOIMarkers(); // 显示搜索范围圆 if (state.searchCircle) { state.map.remove(state.searchCircle); } state.searchCircle = new AMap.Circle({ center: [state.center.lng, state.center.lat], radius: radius, fillColor: '#f59e0b', fillOpacity: 0.1, strokeColor: '#f59e0b', strokeWeight: 2, strokeOpacity: 0.6 }); state.map.add(state.searchCircle); // 添加POI标记 pois.forEach((poi, index) => { const marker = new AMap.Marker({ position: [poi.location.lng, poi.location.lat], content: `
    ${index + 1}
    `, offset: new AMap.Pixel(-16, -16), zIndex: 100 }); marker.on('click', () => { showPOIInfoWindow(marker, poi); highlightResult(index); }); state.poiMarkers.push(marker); state.map.add(marker); }); // 渲染结果列表 renderResultList(pois); // 调整视野(右侧留出空间给浮动面板) const allMarkers = [...state.markers, state.centerMarker, ...state.poiMarkers]; state.map.setFitView(allMarkers, false, [80, 420, 80, 80]); } function renderResultList(pois) { const panel = document.getElementById('floatingResults'); const list = document.getElementById('floatingResultList'); const count = document.getElementById('floatingResultCount'); const filterInput = document.getElementById('resultFilter'); // 保存POI数据供筛选使用 state.currentPOIs = pois; count.textContent = pois.length; if (pois.length === 0) { list.innerHTML = `
  • 😕 未找到相关地点 尝试增大搜索半径或更换关键词
  • `; } else { list.innerHTML = pois.map((poi, index) => { const dist = parseDistance(poi.distance); return `
  • ${index + 1}
    ${poi.name}
    ${poi.address || poi.type}
    ${poi.tel ? `` : ''}
    ${dist.value} ${dist.unit}
  • `}).join(''); // 绑定点击事件 list.querySelectorAll('.floating-result-item').forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const index = parseInt(item.dataset.index); focusPOI(index); }); }); } panel.style.display = 'flex'; filterInput.value = ''; // 绑定关闭按钮事件 document.getElementById('closeFloatingResults').onclick = () => { panel.style.display = 'none'; }; // 绑定筛选事件 filterInput.oninput = (e) => { const keyword = e.target.value.trim().toLowerCase(); filterResults(keyword); }; } // 解析距离为数值和单位 function parseDistance(distance) { const d = parseInt(distance); if (d >= 1000) { return { value: (d / 1000).toFixed(1), unit: '公里' }; } return { value: d, unit: '米' }; } // 筛选结果 function filterResults(keyword) { const items = document.querySelectorAll('.floating-result-item'); let visibleCount = 0; items.forEach(item => { const name = item.dataset.name?.toLowerCase() || ''; const address = item.querySelector('.address')?.textContent.toLowerCase() || ''; if (!keyword || name.includes(keyword) || address.includes(keyword)) { item.classList.remove('hidden'); visibleCount++; } else { item.classList.add('hidden'); } }); // 更新显示数量 document.getElementById('floatingResultCount').textContent = visibleCount; } function formatDistance(distance) { const d = parseInt(distance); if (d >= 1000) { return (d / 1000).toFixed(1) + ' 公里'; } return d + ' 米'; } function focusPOI(index) { const marker = state.poiMarkers[index]; const poi = state.currentPOIs[index]; if (marker && poi) { // 先高亮结果 highlightResult(index); // 关闭当前信息窗口 state.infoWindow.close(); // 使用 panTo 平滑移动到目标位置 const position = marker.getPosition(); state.map.panTo(position); // 延迟设置缩放和打开信息窗口,确保地图移动完成 setTimeout(() => { state.map.setZoom(16); showPOIInfoWindow(marker, poi); }, 300); } } function highlightResult(index) { document.querySelectorAll('.floating-result-item').forEach((item, i) => { item.classList.toggle('active', i === index); }); // 滚动到对应项 const activeItem = document.querySelector(`.floating-result-item[data-index="${index}"]`); if (activeItem) { activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } } // ======================================== // 信息窗口 // ======================================== function showInfoWindow(marker, location) { const content = `

    ${location.name}

    ${location.address || '暂无地址'}

    `; state.infoWindow.setContent(content); state.infoWindow.open(state.map, marker.getPosition()); } function showPOIInfoWindow(marker, poi) { const distText = formatDistance(poi.distance); const navUrl = `https://uri.amap.com/navigation?to=${poi.location.lng},${poi.location.lat},${encodeURIComponent(poi.name)}&mode=car&coordinate=gaode`; const content = `

    ${poi.name}

    🎯 距中心点: ${distText}

    ${poi.address ? `

    📍 ${poi.address}

    ` : ''} ${poi.tel ? `

    📞 ${poi.tel}

    ` : ''} 导航前往
    `; state.infoWindow.setContent(content); state.infoWindow.open(state.map, marker.getPosition()); } // ======================================== // 工具函数 // ======================================== function clearSearchResults() { // 隐藏结果区域 document.getElementById('floatingResults').style.display = 'none'; document.getElementById('centerInfo').style.display = 'none'; document.getElementById('resultFilter').value = ''; // 清除标记 clearPOIMarkers(); if (state.centerMarker) { state.map.remove(state.centerMarker); state.centerMarker = null; } if (state.searchCircle) { state.map.remove(state.searchCircle); state.searchCircle = null; } state.center = null; state.currentPOIs = []; } function clearPOIMarkers() { state.poiMarkers.forEach(marker => { state.map.remove(marker); }); state.poiMarkers = []; } function showLoading(show) { document.getElementById('loadingOverlay').style.display = show ? 'flex' : 'none'; } // ======================================== // 暴露全局函数 // ======================================== window.removeLocation = removeLocation; window.focusPOI = focusPOI;