From e3579ee13918ace582a798c873e41ee724e28727 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 1 Oct 2026 15:18:18 +0800 Subject: [PATCH] =?UTF-8?q?AI=20=E7=94=9F=E6=88=90:=20=E5=9F=BA=E4=BA=8EAI?= =?UTF-8?q?=E7=9A=84=E9=85=92=E5=BA=97=E6=8E=A8=E8=8D=90=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=EF=BC=8C=E9=9B=86=E6=88=90=E5=9C=B0=E5=9B=BE=E8=B7=AF=E7=BA=BF?= =?UTF-8?q?=E4=B8=8E=E8=B7=9D=E7=A6=BB=E6=98=BE=E7=A4=BA=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8Flask=E5=90=8E=E7=AB=AF=E4=B8=8ELeaflet=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 31 ++++++++++++++++++ app.py | 69 ++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 ++ static/css/style.css | 26 +++++++++++++++ static/js/main.js | 76 ++++++++++++++++++++++++++++++++++++++++++++ templates/index.html | 50 +++++++++++++++++++++++++++++ 6 files changed, 254 insertions(+) create mode 100644 README.md create mode 100644 app.py create mode 100644 requirements.txt create mode 100644 static/css/style.css create mode 100644 static/js/main.js create mode 100644 templates/index.html diff --git a/README.md b/README.md new file mode 100644 index 0000000..d504690 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# AI Hotel Recommender + +基于AI的酒店推荐系统,支持地图查看路线与距离。 + +## 功能 +- 输入目的地、入住日期、预算等条件 +- AI推荐酒店列表 +- 地图显示酒店位置、路线与距离 + +## 技术栈 +- 后端: Python Flask +- 前端: HTML/CSS/JS, Leaflet, OpenStreetMap +- AI: 简单规则引擎(可替换为真实API) + +## 运行 +1. 安装依赖: `pip install flask requests` +2. 启动: `python app.py` +3. 访问: http://localhost:5000 + +## 目录结构 +``` +ai-hotel-recommender/ +├── app.py +├── requirements.txt +├── README.md +├── static/ +│ ├── css/style.css +│ └── js/main.js +└── templates/ + └── index.html +``` diff --git a/app.py b/app.py new file mode 100644 index 0000000..c219085 --- /dev/null +++ b/app.py @@ -0,0 +1,69 @@ +from flask import Flask, render_template, request, jsonify +import random +import math + +app = Flask(__name__) + +# 模拟酒店数据(实际可接入真实API) +HOTELS = [ + {"id": 1, "name": "海景大酒店", "lat": 31.2304, "lng": 121.4737, "price": 800, "rating": 4.8, "tags": ["海景", "免费早餐", "健身房"]}, + {"id": 2, "name": "城市商务酒店", "lat": 31.2350, "lng": 121.4800, "price": 500, "rating": 4.5, "tags": ["商务中心", "会议室", "24小时前台"]}, + {"id": 3, "name": "温馨民宿", "lat": 31.2250, "lng": 121.4650, "price": 300, "rating": 4.7, "tags": ["家庭房", "厨房", "免费停车"]}, + {"id": 4, "name": "豪华度假村", "lat": 31.2400, "lng": 121.4900, "price": 1500, "rating": 4.9, "tags": ["泳池", "SPA", "私人海滩"]}, + {"id": 5, "name": "经济快捷酒店", "lat": 31.2200, "lng": 121.4600, "price": 200, "rating": 4.2, "tags": ["免费WiFi", "空调", "电视"]}, +] + +def haversine(lat1, lon1, lat2, lon2): + """计算两点间距离(公里)""" + R = 6371 + dlat = math.radians(lat2 - lat1) + dlon = math.radians(lon2 - lon1) + a = math.sin(dlat/2)**2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon/2)**2 + c = 2 * math.asin(math.sqrt(a)) + return R * c + +def ai_recommend(destination, budget, preferences, user_lat, user_lng): + """简单AI推荐:根据预算、偏好、距离评分""" + scored = [] + for h in HOTELS: + score = 0 + # 预算匹配 + if budget and h["price"] <= budget: + score += 30 + elif budget: + score -= 10 + # 偏好匹配 + for pref in preferences: + if pref in h["tags"]: + score += 20 + # 评分 + score += h["rating"] * 5 + # 距离(越近越高) + dist = haversine(user_lat, user_lng, h["lat"], h["lng"]) + score += max(0, 30 - dist * 2) + h_copy = h.copy() + h_copy["distance"] = round(dist, 2) + h_copy["score"] = round(score, 1) + scored.append(h_copy) + scored.sort(key=lambda x: x["score"], reverse=True) + return scored + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/api/recommend', methods=['POST']) +def recommend(): + data = request.json + destination = data.get('destination', '上海') + budget = data.get('budget') + preferences = data.get('preferences', []) + user_lat = data.get('lat', 31.2304) + user_lng = data.get('lng', 121.4737) + if budget: + budget = float(budget) + results = ai_recommend(destination, budget, preferences, user_lat, user_lng) + return jsonify({"hotels": results}) + +if __name__ == '__main__': + app.run(debug=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b509766 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask==2.3.3 +requests==2.31.0 diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..6bd72af --- /dev/null +++ b/static/css/style.css @@ -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%; } diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..643c2ef --- /dev/null +++ b/static/js/main.js @@ -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 = ` +

${h.name}

+
⭐ ${h.rating}¥${h.price}/晚
+
📏 ${h.distance} km🎯 ${h.score}分
+
${h.tags.map(t => `${t}`).join('')}
+ `; + 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(`${h.name}
¥${h.price}/晚 · ⭐${h.rating}
距离: ${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); +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..888b712 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,50 @@ + + + + + + AI 酒店推荐 + + + + +
+
+

🏨 AI 酒店推荐

+

智能推荐 · 地图看路线 · 距离一目了然

+
+
+ +
+
+
+
+
+ + + +