AI 生成: 基于AI的酒店推荐系统,集成地图路线与距离显示,使用Flask后端与Leaflet前端。
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f0f2f5; color: #333; }
|
||||
.container { display: flex; flex-direction: column; height: 100vh; }
|
||||
header { background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 16px 24px; }
|
||||
header h1 { font-size: 22px; }
|
||||
header p { font-size: 13px; opacity: 0.9; margin-top: 4px; }
|
||||
.main { display: flex; flex: 1; overflow: hidden; }
|
||||
.sidebar { width: 340px; background: white; padding: 20px; overflow-y: auto; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
|
||||
.form-group { margin-bottom: 16px; }
|
||||
.form-group label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: #555; }
|
||||
.form-group input[type="text"], .form-group input[type="number"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; }
|
||||
.checkboxes { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.checkboxes label { font-size: 13px; display: flex; align-items: center; gap: 4px; background: #f5f5f5; padding: 6px 10px; border-radius: 20px; cursor: pointer; }
|
||||
button#searchBtn { width: 100%; padding: 12px; background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: opacity 0.2s; }
|
||||
button#searchBtn:hover { opacity: 0.9; }
|
||||
.hotel-list { margin-top: 20px; }
|
||||
.hotel-card { background: #fafafa; border: 1px solid #eee; border-radius: 10px; padding: 12px; margin-bottom: 10px; cursor: pointer; transition: all 0.2s; }
|
||||
.hotel-card:hover { border-color: #667eea; box-shadow: 0 2px 8px rgba(102,126,234,0.15); }
|
||||
.hotel-card.active { border-color: #667eea; background: #f0f4ff; }
|
||||
.hotel-card h3 { font-size: 15px; margin-bottom: 4px; }
|
||||
.hotel-card .meta { font-size: 12px; color: #888; display: flex; justify-content: space-between; }
|
||||
.hotel-card .tags { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.hotel-card .tags span { font-size: 11px; background: #e8ecff; color: #5566cc; padding: 2px 8px; border-radius: 10px; }
|
||||
.placeholder { color: #aaa; font-size: 13px; text-align: center; padding: 20px 0; }
|
||||
.map-container { flex: 1; }
|
||||
#map { width: 100%; height: 100%; }
|
||||
@@ -0,0 +1,76 @@
|
||||
// 初始化地图
|
||||
const map = L.map('map').setView([31.2304, 121.4737], 13);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
let markers = [];
|
||||
let routeLine = null;
|
||||
let userMarker = null;
|
||||
|
||||
// 获取用户位置(模拟为上海中心)
|
||||
const userLat = 31.2304, userLng = 121.4737;
|
||||
userMarker = L.marker([userLat, userLng], { icon: L.divIcon({ className: 'user-marker', html: '📍', iconSize: [24, 24] }) }).addTo(map).bindPopup('您的位置');
|
||||
|
||||
// 搜索按钮
|
||||
document.getElementById('searchBtn').addEventListener('click', async () => {
|
||||
const destination = document.getElementById('destination').value;
|
||||
const budget = document.getElementById('budget').value;
|
||||
const preferences = Array.from(document.querySelectorAll('.checkboxes input:checked')).map(cb => cb.value);
|
||||
|
||||
const res = await fetch('/api/recommend', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ destination, budget, preferences, lat: userLat, lng: userLng })
|
||||
});
|
||||
const data = await res.json();
|
||||
renderHotels(data.hotels);
|
||||
renderMarkers(data.hotels);
|
||||
});
|
||||
|
||||
function renderHotels(hotels) {
|
||||
const list = document.getElementById('hotelList');
|
||||
list.innerHTML = '';
|
||||
hotels.forEach((h, i) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'hotel-card';
|
||||
card.innerHTML = `
|
||||
<h3>${h.name}</h3>
|
||||
<div class="meta"><span>⭐ ${h.rating}</span><span>¥${h.price}/晚</span></div>
|
||||
<div class="meta"><span>📏 ${h.distance} km</span><span>🎯 ${h.score}分</span></div>
|
||||
<div class="tags">${h.tags.map(t => `<span>${t}</span>`).join('')}</div>
|
||||
`;
|
||||
card.addEventListener('click', () => {
|
||||
document.querySelectorAll('.hotel-card').forEach(c => c.classList.remove('active'));
|
||||
card.classList.add('active');
|
||||
showRoute(h);
|
||||
});
|
||||
list.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function renderMarkers(hotels) {
|
||||
markers.forEach(m => map.removeLayer(m));
|
||||
markers = [];
|
||||
if (routeLine) { map.removeLayer(routeLine); routeLine = null; }
|
||||
const bounds = [[userLat, userLng]];
|
||||
hotels.forEach(h => {
|
||||
const marker = L.marker([h.lat, h.lng]).addTo(map)
|
||||
.bindPopup(`<b>${h.name}</b><br>¥${h.price}/晚 · ⭐${h.rating}<br>距离: ${h.distance} km`);
|
||||
markers.push(marker);
|
||||
bounds.push([h.lat, h.lng]);
|
||||
});
|
||||
map.fitBounds(bounds, { padding: [50, 50] });
|
||||
}
|
||||
|
||||
function showRoute(hotel) {
|
||||
if (routeLine) map.removeLayer(routeLine);
|
||||
// 直线路线(实际可接入路由API)
|
||||
const points = [[userLat, userLng], [hotel.lat, hotel.lng]];
|
||||
routeLine = L.polyline(points, { color: '#667eea', weight: 4, dashArray: '8,8' }).addTo(map);
|
||||
map.fitBounds(routeLine.getBounds(), { padding: [50, 50] });
|
||||
L.popup()
|
||||
.setLatLng([(userLat + hotel.lat) / 2, (userLng + hotel.lng) / 2])
|
||||
.setContent(`距离: ${hotel.distance} km`)
|
||||
.openOn(map);
|
||||
}
|
||||
Reference in New Issue
Block a user