AI 生成: 一个面向个人/团队/家庭的智能旅游规划工具,支持AI行程生成、地图标注与景点打卡记录。

This commit is contained in:
eeuse
2026-09-29 04:31:48 +08:00
commit 7e7bd600e4
8 changed files with 678 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
// AI 行程生成:本地规则引擎(可替换为真实大模型 API)
(function () {
// 一些常见城市的示例景点库
const SPOT_DB = {
'杭州': {
'自然风光': ['西湖', '西溪湿地', '九溪烟树'],
'历史文化': ['灵隐寺', '雷峰塔', '南宋御街'],
'美食购物': ['河坊街', '武林夜市', '湖滨银泰'],
'亲子家庭': ['杭州动物园', '极地海洋公园', '少年儿童公园']
},
'北京': {
'自然风光': ['颐和园', '香山公园', '奥林匹克森林公园'],
'历史文化': ['故宫', '天坛', '八达岭长城'],
'美食购物': ['南锣鼓巷', '王府井', '簋街'],
'亲子家庭': ['北京动物园', '欢乐谷', '中国科技馆']
},
'成都': {
'自然风光': ['青城山', '都江堰', '西岭雪山'],
'历史文化': ['武侯祠', '杜甫草堂', '金沙遗址'],
'美食购物': ['宽窄巷子', '锦里', '春熙路'],
'亲子家庭': ['成都动物园', '大熊猫繁育研究基地', '欢乐谷']
},
'上海': {
'自然风光': ['外滩', '世纪公园', '佘山'],
'历史文化': ['豫园', '城隍庙', '中共一大会址'],
'美食购物': ['南京路', '新天地', '田子坊'],
'亲子家庭': ['上海动物园', '迪士尼乐园', '上海科技馆']
}
};
// 默认通用景点(当城市不在库中时使用)
const DEFAULT_SPOTS = {
'自然风光': ['城市公园', '滨江步道', '近郊山景'],
'历史文化': ['博物馆', '老城区', '历史街区'],
'美食购物': ['美食街', '商业中心', '夜市'],
'亲子家庭': ['动物园', '科技馆', '游乐场']
};
// 生成行程计划
function generatePlan(dest, days, pref) {
const city = (dest || '').trim();
const db = SPOT_DB[city] || DEFAULT_SPOTS;
const pool = db[pref] || DEFAULT_SPOTS[pref] || DEFAULT_SPOTS['自然风光'];
const lines = [];
lines.push(`📍 目的地:${city || '未指定'}`);
lines.push(`🗓️ 天数:${days} 天`);
lines.push(`🎯 偏好:${pref}`);
lines.push('');
for (let d = 1; d <= days; d++) {
lines.push(`—— 第 ${d} 天 ——`);
const morning = pool[(d - 1) % pool.length];
const afternoon = pool[(d) % pool.length];
const evening = pool[(d + 1) % pool.length];
lines.push(`上午:游览「${morning}」`);
lines.push(`下午:前往「${afternoon}」`);
lines.push(`晚上:在「${evening}」附近用餐 / 散步`);
lines.push('');
}
lines.push('💡 提示:可在地图上点击添加自定义景点并标注状态。');
return lines.join('\n');
}
// 对外接口(异步,方便未来替换为真实 API)
function generatePlanAsync(dest, days, pref) {
return new Promise(resolve => {
setTimeout(() => resolve(generatePlan(dest, days, pref)), 400);
});
}
window.AI = { generatePlanAsync };
})();
+45
View File
@@ -0,0 +1,45 @@
// 简单的本地账号系统:用户数据保存在 localStorage
(function () {
const USERS_KEY = 'tp_users';
const SESSION_KEY = 'tp_session';
function getUsers() {
try { return JSON.parse(localStorage.getItem(USERS_KEY)) || {}; }
catch (e) { return {}; }
}
function saveUsers(users) {
localStorage.setItem(USERS_KEY, JSON.stringify(users));
}
const Auth = {
currentUser() {
return localStorage.getItem(SESSION_KEY) || null;
},
login(username, password) {
const users = getUsers();
if (!users[username]) return { ok: false, msg: '用户不存在,请先注册' };
if (users[username] !== password) return { ok: false, msg: '密码错误' };
localStorage.setItem(SESSION_KEY, username);
return { ok: true };
},
register(username, password) {
if (!username || !password) return { ok: false, msg: '用户名和密码不能为空' };
const users = getUsers();
if (users[username]) return { ok: false, msg: '用户已存在' };
users[username] = password;
saveUsers(users);
localStorage.setItem(SESSION_KEY, username);
return { ok: true };
},
logout() {
localStorage.removeItem(SESSION_KEY);
},
// 每个用户独立的数据命名空间
dataKey(name) {
const u = this.currentUser();
return u ? `tp_data_${u}_${name}` : null;
}
};
window.Auth = Auth;
})();
+156
View File
@@ -0,0 +1,156 @@
// 主入口:串联 UI、认证、地图与 AI
(function () {
const $ = sel => document.querySelector(sel);
// ================== 用户界面 ==================
function renderUserArea() {
const user = window.Auth.currentUser();
const area = $('#userArea');
if (user) {
area.innerHTML = `<span style="margin-right:10px;font-size:13px">👤 ${user}</span>` +
`<button id="logoutBtn" class="btn">退出</button>`;
$('#logoutBtn').addEventListener('click', () => {
window.Auth.logout();
renderUserArea();
loadSpots();
});
} else {
area.innerHTML = `<button id="loginBtn" class="btn">登录 / 注册</button>`;
$('#loginBtn').addEventListener('click', openModal);
}
}
// ================== 弹窗 ==================
function openModal() {
$('#loginModal').classList.remove('hidden');
}
function closeModal() {
$('#loginModal').classList.add('hidden');
$('#usernameInput').value = '';
$('#passwordInput').value = '';
}
function bindModal() {
$('#closeModal').addEventListener('click', closeModal);
$('#doLogin').addEventListener('click', () => {
const u = $('#usernameInput').value.trim();
const p = $('#passwordInput').value;
const res = window.Auth.login(u, p);
if (!res.ok) return alert(res.msg);
closeModal();
renderUserArea();
loadSpots();
});
$('#doRegister').addEventListener('click', () => {
const u = $('#usernameInput').value.trim();
const p = $('#passwordInput').value;
const res = window.Auth.register(u, p);
if (!res.ok) return alert(res.msg);
closeModal();
renderUserArea();
loadSpots();
});
}
// ================== 景点数据存储 ==================
function loadSpots() {
const key = window.Auth.dataKey('spots');
if (!key) {
// 未登录:清空地图
window.MapAPI.setSpots([]);
renderSpotList([]);
return;
}
let spots = [];
try { spots = JSON.parse(localStorage.getItem(key)) || []; }
catch (e) { spots = []; }
window.MapAPI.setSpots(spots);
renderSpotList(spots);
}
function saveSpots(spots) {
const key = window.Auth.dataKey('spots');
if (!key) return;
localStorage.setItem(key, JSON.stringify(spots));
}
// 当 MapAPI 内部数据变化时回调
window.MapAPI.onSpotsChange = function (spots) {
saveSpots(spots);
renderSpotList(spots);
};
// ================== 景点列表渲染 ==================
let currentFilter = 'all';
function renderSpotList(spots) {
const list = $('#spotList');
list.innerHTML = '';
const filtered = spots.filter(s => {
if (currentFilter === 'all') return true;
return s.status === currentFilter;
});
if (filtered.length === 0) {
list.innerHTML = '<li style="color:#94a3b8">暂无景点</li>';
return;
}
filtered.forEach(s => {
const li = document.createElement('li');
li.innerHTML = `<span>${s.name}</span>` +
`<span class="status ${s.status}">${s.status === 'visited' ? '去过' : '想去'}</span>`;
li.style.cursor = 'pointer';
li.addEventListener('click', () => {
window.MapAPI.flyTo(s.lat, s.lng, 14);
});
list.appendChild(li);
});
}
// ================== 过滤器 ==================
function bindFilter() {
document.querySelectorAll('input[name="filter"]').forEach(el => {
el.addEventListener('change', e => {
currentFilter = e.target.value;
renderSpotList(window.MapAPI.getSpots());
});
});
}
// ================== AI 生成 ==================
function bindAI() {
$('#genBtn').addEventListener('click', async () => {
const dest = $('#destInput').value.trim();
const days = parseInt($('#daysInput').value, 10) || 1;
const pref = $('#prefInput').value;
const result = $('#planResult');
result.textContent = '⏳ 正在生成行程...';
try {
const text = await window.AI.generatePlanAsync(dest, days, pref);
result.textContent = text;
// 顺便把目的地飞到地图中心
const cityCoords = {
'杭州': [30.2741, 120.1551],
'北京': [39.9042, 116.4074],
'成都': [30.5728, 104.0668],
'上海': [31.2304, 121.4737]
};
if (cityCoords[dest]) {
window.MapAPI.flyTo(cityCoords[dest][0], cityCoords[dest][1], 11);
}
} catch (err) {
result.textContent = '生成失败:' + err.message;
}
});
}
// ================== 启动 ==================
function init() {
renderUserArea();
bindModal();
bindFilter();
bindAI();
loadSpots();
}
document.addEventListener('DOMContentLoaded', init);
})();
+96
View File
@@ -0,0 +1,96 @@
// 地图与景点标注逻辑
(function () {
// 初始化地图:默认定位到中国杭州
const map = L.map('map').setView([30.2741, 120.1551], 11);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; OpenStreetMap'
}).addTo(map);
// 保存所有标注:{ id, name, lat, lng, status: 'visited' | 'wish' }
let spots = [];
const markers = {}; // id -> L.Marker
// 根据状态生成不同颜色图标
function makeIcon(status) {
const color = status === 'visited' ? '#16a34a' : '#f59e0b';
return L.divIcon({
className: '',
html: `<div style="background:${color};width:16px;height:16px;border-radius:50%;border:2px solid #fff;box-shadow:0 0 4px rgba(0,0,0,.4)"></div>`,
iconSize: [16, 16],
iconAnchor: [8, 8]
});
}
function renderSpot(spot) {
if (markers[spot.id]) {
map.removeLayer(markers[spot.id]);
}
const marker = L.marker([spot.lat, spot.lng], { icon: makeIcon(spot.status) }).addTo(map);
marker.bindPopup(
`<b>${spot.name}</b><br/>状态:${spot.status === 'visited' ? '去过 ✅' : '想去 ⭐'}<br/>` +
`<button data-id="${spot.id}" class="popup-toggle">切换状态</button>`
);
markers[spot.id] = marker;
}
function renderAll() {
// 清除旧 marker
Object.values(markers).forEach(m => map.removeLayer(m));
Object.keys(markers).forEach(k => delete markers[k]);
spots.forEach(renderSpot);
}
// 地图点击添加新景点
map.on('click', function (e) {
const user = window.Auth.currentUser();
if (!user) {
alert('请先登录后再添加景点标注');
return;
}
const name = prompt('请输入景点名称:');
if (!name) return;
const spot = {
id: 's_' + Date.now() + '_' + Math.floor(Math.random() * 1000),
name: name.trim(),
lat: e.latlng.lat,
lng: e.latlng.lng,
status: 'wish'
};
spots.push(spot);
renderSpot(spot);
window.MapAPI.onSpotsChange(spots);
});
// 弹窗内切换状态
map.on('popupopen', function (e) {
const btn = e.popup.getElement().querySelector('.popup-toggle');
if (!btn) return;
btn.addEventListener('click', function () {
const id = btn.getAttribute('data-id');
const spot = spots.find(s => s.id === id);
if (!spot) return;
spot.status = spot.status === 'visited' ? 'wish' : 'visited';
renderSpot(spot);
window.MapAPI.onSpotsChange(spots);
map.closePopup();
});
});
// 对外暴露的 API
window.MapAPI = {
getMap: () => map,
getSpots: () => spots,
setSpots(newSpots) {
spots = newSpots || [];
renderAll();
},
// 由 main.js 覆盖,用于同步到 UI 和存储
onSpotsChange: function () {},
// 定位到坐标
flyTo(lat, lng, zoom) {
map.flyTo([lat, lng], zoom || 12);
}
};
})();