1081 lines
36 KiB
JavaScript
1081 lines
36 KiB
JavaScript
/**
|
||
* 会面点 - 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();
|
||
initMobileUI();
|
||
});
|
||
|
||
// ========================================
|
||
// 移动端UI初始化
|
||
// ========================================
|
||
|
||
function initMobileUI() {
|
||
const sidebar = document.getElementById('sidebar');
|
||
const overlay = document.getElementById('sidebarOverlay');
|
||
const menuBtn = document.getElementById('menuBtn');
|
||
const mobileSearchBtn = document.getElementById('mobileSearchBtn');
|
||
const mobileActionBar = document.getElementById('mobileActionBar');
|
||
const mobileSearchInput = document.getElementById('mobileSearchInput');
|
||
const mobileSearchClear = document.getElementById('mobileSearchClear');
|
||
const mobileSearchTips = document.getElementById('mobileSearchTips');
|
||
const floatingResultBtn = document.getElementById('floatingResultBtn');
|
||
const floatingResultBadge = document.getElementById('floatingResultBadge');
|
||
|
||
if (!sidebar || !overlay) return;
|
||
|
||
// 切换侧边栏
|
||
function toggleSidebar() {
|
||
sidebar.classList.toggle('active');
|
||
overlay.classList.toggle('active');
|
||
// 打开侧边栏时隐藏浮动按钮
|
||
if (sidebar.classList.contains('active') && floatingResultBtn) {
|
||
floatingResultBtn.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 关闭侧边栏
|
||
function closeSidebar() {
|
||
sidebar.classList.remove('active');
|
||
overlay.classList.remove('active');
|
||
// 关闭侧边栏时,如果有搜索结果且结果面板隐藏,显示浮动按钮
|
||
const panel = document.getElementById('floatingResults');
|
||
if (floatingResultBtn && state.currentPOIs && state.currentPOIs.length > 0 &&
|
||
(!panel || panel.style.display === 'none')) {
|
||
window.showFloatingResultBtn(state.currentPOIs.length);
|
||
}
|
||
}
|
||
|
||
// 菜单按钮点击
|
||
if (menuBtn) {
|
||
menuBtn.addEventListener('click', toggleSidebar);
|
||
}
|
||
|
||
// 遮罩点击关闭
|
||
overlay.addEventListener('click', closeSidebar);
|
||
|
||
// 移动端搜索按钮
|
||
if (mobileSearchBtn) {
|
||
mobileSearchBtn.addEventListener('click', handleSearch);
|
||
}
|
||
|
||
// 移动端地址搜索
|
||
if (mobileSearchInput) {
|
||
let searchTimeout;
|
||
|
||
mobileSearchInput.addEventListener('input', (e) => {
|
||
const value = e.target.value.trim();
|
||
mobileSearchClear.style.display = value ? 'block' : 'none';
|
||
|
||
clearTimeout(searchTimeout);
|
||
if (value.length >= 2) {
|
||
searchTimeout = setTimeout(() => fetchMobileSearchTips(value), 300);
|
||
} else {
|
||
mobileSearchTips.classList.remove('active');
|
||
}
|
||
});
|
||
|
||
mobileSearchInput.addEventListener('focus', () => {
|
||
if (mobileSearchInput.value.trim().length >= 2) {
|
||
mobileSearchTips.classList.add('active');
|
||
}
|
||
});
|
||
|
||
mobileSearchClear.addEventListener('click', () => {
|
||
mobileSearchInput.value = '';
|
||
mobileSearchClear.style.display = 'none';
|
||
mobileSearchTips.classList.remove('active');
|
||
});
|
||
|
||
// 点击外部关闭搜索提示
|
||
document.addEventListener('click', (e) => {
|
||
if (!e.target.closest('.mobile-search-bar')) {
|
||
mobileSearchTips.classList.remove('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 浮动结果按钮点击 - 重新打开结果面板
|
||
if (floatingResultBtn) {
|
||
floatingResultBtn.addEventListener('click', () => {
|
||
const panel = document.getElementById('floatingResults');
|
||
if (panel && state.currentPOIs.length > 0) {
|
||
panel.style.display = 'flex';
|
||
floatingResultBtn.style.display = 'none';
|
||
if (mobileActionBar) {
|
||
mobileActionBar.style.display = 'none';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 点击搜索结果后自动关闭侧边栏(移动端)
|
||
window.closeSidebarOnMobile = function() {
|
||
if (window.innerWidth <= 768) {
|
||
closeSidebar();
|
||
}
|
||
};
|
||
|
||
// 隐藏/显示移动端操作栏
|
||
window.toggleMobileActionBar = function(show) {
|
||
if (mobileActionBar && window.innerWidth <= 768) {
|
||
mobileActionBar.style.display = show ? 'flex' : 'none';
|
||
}
|
||
};
|
||
|
||
// 显示浮动结果按钮
|
||
window.showFloatingResultBtn = function(count) {
|
||
if (floatingResultBtn) {
|
||
// 清除所有内联样式,让CSS控制
|
||
floatingResultBtn.style.cssText = 'display: flex;';
|
||
if (floatingResultBadge) {
|
||
floatingResultBadge.textContent = count;
|
||
}
|
||
console.log('显示浮动结果按钮,数量:', count);
|
||
}
|
||
};
|
||
|
||
// 隐藏浮动结果按钮
|
||
window.hideFloatingResultBtn = function() {
|
||
if (floatingResultBtn) {
|
||
floatingResultBtn.style.display = 'none';
|
||
}
|
||
};
|
||
|
||
// 监听窗口大小变化
|
||
window.addEventListener('resize', () => {
|
||
if (window.innerWidth > 768) {
|
||
closeSidebar();
|
||
if (mobileActionBar) {
|
||
mobileActionBar.style.display = 'none';
|
||
}
|
||
if (floatingResultBtn) {
|
||
floatingResultBtn.style.display = 'none';
|
||
}
|
||
} else {
|
||
const floatingResults = document.getElementById('floatingResults');
|
||
const resultsHidden = !floatingResults || floatingResults.style.display === 'none';
|
||
|
||
if (mobileActionBar && resultsHidden) {
|
||
mobileActionBar.style.display = 'flex';
|
||
}
|
||
|
||
// 如果有搜索结果且结果面板隐藏,显示浮动按钮
|
||
if (resultsHidden && state.currentPOIs && state.currentPOIs.length > 0) {
|
||
window.showFloatingResultBtn(state.currentPOIs.length);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 移动端地址搜索提示
|
||
async function fetchMobileSearchTips(keywords) {
|
||
const mobileSearchTips = document.getElementById('mobileSearchTips');
|
||
|
||
try {
|
||
const response = await fetch(`/api/tips?keywords=${encodeURIComponent(keywords)}&city=`);
|
||
const data = await response.json();
|
||
|
||
if (data.tips && data.tips.length > 0) {
|
||
renderMobileSearchTips(data.tips);
|
||
} else {
|
||
mobileSearchTips.classList.remove('active');
|
||
}
|
||
} catch (error) {
|
||
console.error('获取搜索提示失败:', error);
|
||
}
|
||
}
|
||
|
||
function renderMobileSearchTips(tips) {
|
||
const mobileSearchTips = document.getElementById('mobileSearchTips');
|
||
const mobileSearchInput = document.getElementById('mobileSearchInput');
|
||
const mobileSearchClear = document.getElementById('mobileSearchClear');
|
||
|
||
mobileSearchTips.innerHTML = tips.map(tip => `
|
||
<div class="mobile-search-tip-item" data-lng="${tip.location.lng}" data-lat="${tip.location.lat}" data-name="${tip.name}" data-address="${tip.district}${tip.address}">
|
||
<div class="tip-name">${tip.name}</div>
|
||
<div class="tip-address">${tip.district}${tip.address}</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
// 绑定点击事件
|
||
mobileSearchTips.querySelectorAll('.mobile-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);
|
||
|
||
// 清理搜索框
|
||
mobileSearchInput.value = '';
|
||
mobileSearchClear.style.display = 'none';
|
||
mobileSearchTips.classList.remove('active');
|
||
});
|
||
});
|
||
|
||
mobileSearchTips.classList.add('active');
|
||
}
|
||
|
||
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);
|
||
|
||
// 先IP定位快速显示,再尝试精确定位
|
||
AMap.plugin(['AMap.CitySearch', 'AMap.Geolocation'], () => {
|
||
// 1. 先IP定位(快速)
|
||
const citySearch = new AMap.CitySearch();
|
||
citySearch.getLocalCity((status, result) => {
|
||
if (status === 'complete' && result.info === 'OK') {
|
||
const bounds = result.bounds;
|
||
if (bounds) {
|
||
state.map.setBounds(bounds);
|
||
}
|
||
console.log('IP定位成功,城市:', result.city);
|
||
}
|
||
});
|
||
|
||
// 2. 然后尝试精确定位(较慢但更准确)
|
||
const geolocation = new AMap.Geolocation({
|
||
enableHighAccuracy: true,
|
||
timeout: 10000,
|
||
buttonPosition: 'RB',
|
||
buttonOffset: new AMap.Pixel(10, 20),
|
||
zoomToAccuracy: true
|
||
});
|
||
|
||
// 添加定位控件到地图
|
||
state.map.addControl(geolocation);
|
||
|
||
geolocation.getCurrentPosition((status, result) => {
|
||
if (status === 'complete' && result.position) {
|
||
state.map.setCenter(result.position);
|
||
state.map.setZoom(15);
|
||
console.log('精确定位成功:', result.formattedAddress);
|
||
} else {
|
||
console.log('精确定位失败,使用IP定位结果');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ========================================
|
||
// 事件绑定
|
||
// ========================================
|
||
|
||
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 => `
|
||
<div class="search-tip-item" data-lng="${tip.location.lng}" data-lat="${tip.location.lat}" data-name="${tip.name}" data-address="${tip.district}${tip.address}">
|
||
<div class="name">${tip.name}</div>
|
||
<div class="address">${tip.district}${tip.address}</div>
|
||
</div>
|
||
`).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');
|
||
}
|
||
|
||
// ========================================
|
||
// 地图点击添加位置
|
||
// ========================================
|
||
|
||
// 待确认的位置
|
||
let pendingLocation = null;
|
||
|
||
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 showMapClickConfirm(address) {
|
||
const confirmDialog = document.getElementById('mapClickConfirm');
|
||
const confirmAddress = document.getElementById('confirmAddress');
|
||
const confirmOk = document.getElementById('confirmOk');
|
||
const confirmCancel = document.getElementById('confirmCancel');
|
||
|
||
if (!confirmDialog) return;
|
||
|
||
confirmAddress.textContent = address;
|
||
confirmDialog.style.display = 'flex';
|
||
|
||
// 确认添加
|
||
confirmOk.onclick = () => {
|
||
if (pendingLocation) {
|
||
addLocation(pendingLocation);
|
||
pendingLocation = null;
|
||
}
|
||
confirmDialog.style.display = 'none';
|
||
};
|
||
|
||
// 取消
|
||
confirmCancel.onclick = () => {
|
||
pendingLocation = null;
|
||
confirmDialog.style.display = 'none';
|
||
};
|
||
|
||
// 点击背景关闭
|
||
confirmDialog.onclick = (e) => {
|
||
if (e.target === confirmDialog) {
|
||
pendingLocation = null;
|
||
confirmDialog.style.display = 'none';
|
||
}
|
||
};
|
||
}
|
||
|
||
// ========================================
|
||
// 位置管理
|
||
// ========================================
|
||
|
||
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: `<div class="custom-marker" style="background: ${color}">${index + 1}</div>`,
|
||
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(`<div class="custom-marker" style="background: ${color}">${i + 1}</div>`);
|
||
});
|
||
|
||
// 更新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 = `
|
||
<li class="empty-state">
|
||
<span class="empty-icon">🗺️</span>
|
||
<span>还没有添加位置</span>
|
||
<span class="empty-hint">搜索地址或点击地图开始</span>
|
||
</li>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
listEl.innerHTML = state.locations.map((loc, index) => {
|
||
const color = state.colors[index % state.colors.length];
|
||
return `
|
||
<li class="location-item">
|
||
<div class="location-marker" style="background: ${color}">${index + 1}</div>
|
||
<div class="location-info">
|
||
<div class="location-name">${loc.name}</div>
|
||
<div class="location-address">${loc.address || '暂无地址'}</div>
|
||
</div>
|
||
<button class="location-remove" onclick="removeLocation(${index})">×</button>
|
||
</li>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function updateSearchButton() {
|
||
const btn = document.getElementById('searchBtn');
|
||
const mobileBtn = document.getElementById('mobileSearchBtn');
|
||
const mobileCount = document.getElementById('mobileLocationCount');
|
||
|
||
const count = state.locations.length;
|
||
const disabled = count < 2;
|
||
btn.disabled = disabled;
|
||
|
||
if (mobileBtn) {
|
||
mobileBtn.disabled = disabled;
|
||
// 更新按钮文字显示当前状态
|
||
if (count === 0) {
|
||
mobileBtn.textContent = '🔍 搜索会面点';
|
||
} else if (count === 1) {
|
||
mobileBtn.textContent = '🔍 再添加1个位置';
|
||
} else {
|
||
mobileBtn.textContent = '🔍 搜索会面点';
|
||
}
|
||
}
|
||
if (mobileCount) {
|
||
mobileCount.textContent = count;
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// 搜索功能
|
||
// ========================================
|
||
|
||
async function handleSearch() {
|
||
const keywords = document.getElementById('poiKeywords').value.trim();
|
||
const radius = parseInt(document.getElementById('searchRadius').value);
|
||
|
||
if (!keywords) {
|
||
// 自动打开侧边栏设置面板
|
||
const sidebar = document.getElementById('sidebar');
|
||
const overlay = document.getElementById('sidebarOverlay');
|
||
if (sidebar && window.innerWidth <= 768) {
|
||
sidebar.classList.add('active');
|
||
if (overlay) overlay.classList.add('active');
|
||
}
|
||
// 聚焦到关键词输入框
|
||
const poiInput = document.getElementById('poiKeywords');
|
||
if (poiInput) {
|
||
poiInput.focus();
|
||
poiInput.setAttribute('placeholder', '⚠️ 请选择或输入搜索类型...');
|
||
// 3秒后恢复原来的placeholder
|
||
setTimeout(() => {
|
||
poiInput.setAttribute('placeholder', '例如:咖啡馆、餐厅、KTV...');
|
||
}, 3000);
|
||
}
|
||
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);
|
||
|
||
// 移动端自动关闭侧边栏
|
||
if (window.closeSidebarOnMobile) {
|
||
window.closeSidebarOnMobile();
|
||
}
|
||
|
||
} 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: '<div class="center-marker">⭐</div>',
|
||
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: `<div class="poi-marker">${index + 1}</div>`,
|
||
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 = `
|
||
<li style="display:flex;flex-direction:column;align-items:center;padding:32px;color:#64748b;text-align:center;">
|
||
<span style="font-size:2.5rem;opacity:0.5;">😕</span>
|
||
<span>未找到相关地点</span>
|
||
<span style="font-size:0.8rem;opacity:0.7;">尝试增大搜索半径或更换关键词</span>
|
||
</li>
|
||
`;
|
||
} else {
|
||
list.innerHTML = pois.map((poi, index) => {
|
||
const dist = parseDistance(poi.distance);
|
||
const rankColors = [
|
||
'background:linear-gradient(135deg,#ffd700,#ffb800);color:#0f0f1a',
|
||
'background:linear-gradient(135deg,#c0c0c0,#a0a0a0);color:#0f0f1a',
|
||
'background:linear-gradient(135deg,#cd7f32,#b87333);color:#0f0f1a'
|
||
];
|
||
const rankBg = rankColors[index] || 'background:#16213e;color:#64748b';
|
||
|
||
return `<li data-index="${index}" data-name="${poi.name}" onclick="focusPOI(${index})" style="
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 12px;
|
||
margin-bottom: 8px;
|
||
background: #0a0a12;
|
||
border: 1px solid #2d2d44;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
">
|
||
<span style="
|
||
flex-shrink: 0;
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 700;
|
||
font-size: 0.85rem;
|
||
margin-right: 12px;
|
||
${rankBg};
|
||
">${index + 1}</span>
|
||
<span style="
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
margin-right: 10px;
|
||
">
|
||
<span style="display:block;font-weight:600;font-size:0.95rem;color:#f8fafc;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${poi.name}</span>
|
||
<span style="display:block;font-size:0.8rem;color:#64748b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${poi.address || poi.type}</span>
|
||
${poi.tel ? `<span style="display:block;font-size:0.8rem;color:#3b82f6;">📞 ${poi.tel}</span>` : ''}
|
||
</span>
|
||
<span style="
|
||
flex-shrink: 0;
|
||
width: 55px;
|
||
padding: 6px 4px;
|
||
text-align: center;
|
||
background: rgba(16,185,129,0.12);
|
||
border-radius: 6px;
|
||
border: 1px solid rgba(16,185,129,0.25);
|
||
">
|
||
<span style="display:block;font-size:1rem;font-weight:700;color:#10b981;">${dist.value}</span>
|
||
<span style="display:block;font-size:0.7rem;color:#10b981;">${dist.unit}</span>
|
||
</span>
|
||
</li>`;
|
||
}).join('');
|
||
|
||
// 点击事件已通过 onclick 内联绑定
|
||
}
|
||
|
||
panel.style.display = 'flex';
|
||
filterInput.value = '';
|
||
|
||
// 移动端隐藏操作栏
|
||
if (window.toggleMobileActionBar) {
|
||
window.toggleMobileActionBar(false);
|
||
}
|
||
|
||
// 绑定关闭按钮事件
|
||
document.getElementById('closeFloatingResults').onclick = () => {
|
||
panel.style.display = 'none';
|
||
// 移动端显示操作栏和浮动结果按钮
|
||
if (window.toggleMobileActionBar) {
|
||
window.toggleMobileActionBar(true);
|
||
}
|
||
if (window.showFloatingResultBtn && state.currentPOIs.length > 0) {
|
||
window.showFloatingResultBtn(state.currentPOIs.length);
|
||
}
|
||
};
|
||
|
||
// 绑定筛选事件
|
||
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);
|
||
|
||
// 移动端:关闭结果面板,显示浮动按钮
|
||
if (window.innerWidth <= 768) {
|
||
const panel = document.getElementById('floatingResults');
|
||
if (panel) {
|
||
panel.style.display = 'none';
|
||
}
|
||
if (window.showFloatingResultBtn) {
|
||
window.showFloatingResultBtn(state.currentPOIs.length);
|
||
}
|
||
if (window.toggleMobileActionBar) {
|
||
window.toggleMobileActionBar(true);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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 = `
|
||
<div class="info-window">
|
||
<h3>${location.name}</h3>
|
||
<p>${location.address || '暂无地址'}</p>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="info-window">
|
||
<h3>${poi.name}</h3>
|
||
<p class="distance">🎯 距中心点: ${distText}</p>
|
||
${poi.address ? `<p>📍 ${poi.address}</p>` : ''}
|
||
${poi.tel ? `<p class="tel">📞 <a href="tel:${poi.tel}">${poi.tel}</a></p>` : ''}
|
||
<a href="${navUrl}" target="_blank" class="nav-btn">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M12 2L4.5 20.29l.71.71L12 18l6.79 3 .71-.71L12 2z"/>
|
||
</svg>
|
||
导航前往
|
||
</a>
|
||
</div>
|
||
`;
|
||
|
||
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 = '';
|
||
|
||
// 移动端显示操作栏,隐藏浮动按钮
|
||
if (window.toggleMobileActionBar) {
|
||
window.toggleMobileActionBar(true);
|
||
}
|
||
if (window.hideFloatingResultBtn) {
|
||
window.hideFloatingResultBtn();
|
||
}
|
||
|
||
// 清除标记
|
||
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;
|