vue3: baidusubway using typescript
项目结构:
<!--npm install -D tailwindcss-3d BaiduSubwayMap.vue npm install -D tailwindcss postcss autoprefixer--> <template> <div class="relative w-full h-screen"> <!-- 地图容器 --> <div id="subway-container" class="w-full h-full"></div> <!-- 缩放控制 --> <div class="fixed bottom-4 right-4 flex flex-col space-y-2 z-10"> <button @click="zoomIn" class="w-10 h-10 rounded-full bg-white shadow-md flex items-center justify-center hover:bg-gray-100 transition-colors"> <i class="fa fa-plus text-gray-700"></i> </button> <button @click="zoomOut" class="w-10 h-10 rounded-full bg-white shadow-md flex items-center justify-center hover:bg-gray-100 transition-colors"> <i class="fa fa-minus text-gray-700"></i> </button> </div> <!-- 地铁图例 --> <div id="legend" class="fixed top-4 right-4 max-w-xs bg-white rounded-lg shadow-lg p-4 hidden md:block z-10"> <h3 class="font-medium text-gray-800 mb-3">地铁线路图例</h3> <div id="legendContent" class="space-y-1 text-sm"> <div v-for="line in subwayLines" :key="line.id" class="flex items-center"> <span class="subway-line" :style="{ backgroundColor: line.color || '#3b82f6' }"></span> <span>{{ line.name }}</span> </div> </div> </div> </div> </template> <script lang="ts"> import { defineComponent, ref, onMounted, onUnmounted, watch} from 'vue'; //,PropType interface SubwayLine { id: string; name: string; color?: string; } interface RouteStep { instruction: string; distance?: number; duration?: number; } interface RouteResult { steps: RouteStep[]; distance?: number; duration?: number; } export default defineComponent({ name: 'BaiduSubwayMap', props: { currentCity: { type: String, required: true }, startStation: { type: String, required: true }, endStation: { type: String, required: true }, cityData: Object as () => Record<string, { start: string; end: string }> //vue 3.3 //Vue 3 //cityData: { //type: Object as PropType<Record<string, { start: string; end: string }>>, //required: true //} }, emits: ['routeFound', 'error', 'mapLoaded'], setup(props, { emit }) { const subway = ref<any>(null); const direction = ref<any>(null); const subwayLines = ref<SubwayLine[]>([]); const isMapLoaded = ref(false); // 监听城市变化 watch(() => props.currentCity, async (newCity, oldCity) => { if (newCity !== oldCity) { console.log(`城市切换: ${oldCity} → ${newCity}`); await loadCitySubway(newCity); } }); // 生命周期钩子 onMounted(() => { initMap(); }); onUnmounted(() => { cleanupSubwayInstance(); }); // 监听城市或站点变化 watch([() => props.currentCity, () => props.startStation, () => props.endStation], () => { if (isMapLoaded.value && props.startStation && props.endStation) { searchRoute(); } }); // 初始化地图 const initMap = () => { try { // 检查百度地图API是否加载成功 if (typeof BMapSub === 'undefined') { emit('error', '百度地图API加载失败,请检查API密钥是否正确'); return; } // 加载当前城市的地铁地图 loadCitySubway(props.currentCity); } catch (error) { console.error('初始化地图时出错:', error); emit('error', '初始化地图时出错,请刷新页面'); } }; // 加载指定城市的地铁地图 const loadCitySubway = (cityName: string) => { // 重置地图容器 const container = document.getElementById('subway-container'); if (container) container.innerHTML = ''; // 清理旧的地铁实例 cleanupSubwayInstance(); try { // 查找城市信息 const city = BMapSub.SubwayCitiesList.find(c => c.name === cityName); if (!city) { emit('error', `未找到${cityName}的地铁数据,请尝试其他城市`); return; } console.log(`加载${cityName}地铁地图,城市代码: ${city.citycode}`); // 创建新的地铁实例 subway.value = new BMapSub.Subway('subway-container', city.citycode); // 绑定地铁加载完成事件 subway.value.addEventListener('subwayloaded', () => { console.log(`${cityName}地铁地图加载完成`); onSubwayLoaded(cityName); emit('mapLoaded', true); }); // 绑定错误处理 subway.value.addEventListener('subwayloaderror', onSubwayLoadError); } catch (e) { console.error('创建地铁实例时出错:', e); emit('error', `加载${cityName}地铁数据失败,请稍后再试`); } }; // 地铁加载完成回调 const onSubwayLoaded = (cityName: string) => { try { // 初始化路线规划 direction.value = new BMapSub.Direction(subway.value); // 设置路线规划完成后的回调 direction.value.addEventListener('directioncomplete', handleRouteResults); isMapLoaded.value = true; emit('mapLoaded', true); // 生成线路图例 generateLineLegend(); // 如果有起点和终点,执行搜索 if (props.startStation && props.endStation) { searchRoute(); } } catch (e) { console.error('初始化地铁地图时出错:', e); emit('error', `初始化${cityName}地铁地图失败,请稍后再试`); } }; // 地铁加载错误回调 const onSubwayLoadError = () => { emit('error', `加载${props.currentCity}地铁数据失败,请稍后再试`); isMapLoaded.value = false; }; // 清理旧的地铁实例 const cleanupSubwayInstance = () => { if (subway.value) { try { subway.value.removeEventListener('subwayloaded', onSubwayLoaded); subway.value.removeEventListener('subwayloaderror', onSubwayLoadError); // 仅在地铁已初始化且有destroy方法时尝试销毁 if (isMapLoaded.value && typeof subway.value.destroy === 'function') { // 移除路线规划器的事件监听器 if (direction.value) { direction.value.removeEventListener('directioncomplete', handleRouteResults); direction.value = null; } // 尝试销毁地铁实例 subway.value.destroy(); } } catch (e) { console.error('销毁地铁实例时出错:', e); } finally { // 无论如何都重置地铁实例和状态 subway.value = null; isMapLoaded.value = false; } } }; // 生成线路图例 const generateLineLegend = () => { try { // 获取线路信息 if (!subway.value) return; const lines = subway.value.getLines(); if (lines && lines.length > 0) { // 只显示前10条线路以避免图例过长 const displayLines = lines.slice(0, 10); subwayLines.value = displayLines.map(line => ({ id: line.id, name: line.name, color: line.color })); } } catch (e) { console.error('生成线路图例时出错:', e); } }; // 搜索路线 const searchRoute = () => { if (!isMapLoaded.value || !direction.value) { emit('error', '地图加载中,请稍候再试'); return; } if (!props.startStation || !props.endStation) { emit('error', '请输入起点站和终点站'); return; } // 验证站点是否属于当前城市 const validStations = getValidStations(props.currentCity); if (validStations && !validStations.includes(props.startStation)) { emit('error', `起点站“${props.startStation}”不存在于${props.currentCity}地铁系统中`); return; } if (validStations && !validStations.includes(props.endStation)) { emit('error', `终点站“${props.endStation}”不存在于${props.currentCity}地铁系统中`); return; } // 执行路线搜索 try { direction.value.search(props.startStation, props.endStation); } catch (e) { console.error('搜索路线时出错:', e); emit('error', '搜索路线时出错,请重试'); } }; // 处理路线规划结果 const handleRouteResults = (results: any) => { try { if (!results || results.length === 0) { emit('error', `未找到从${props.startStation}到${props.endStation}的路线,请尝试其他站点`); return; } // 选择第一条路线(通常是最优路线) const route = results[0]; // 格式化路线结果 const formattedRoute: RouteResult = { steps: route.steps || [], distance: route.distance, duration: route.duration }; // 发送路线结果给父组件 emit('routeFound', formattedRoute); } catch (e) { console.error('处理路线结果时出错:', e); emit('error', '处理路线信息时出错,请重试'); } }; // 地图缩放控制 const zoomIn = () => { if (subway.value) { try { subway.value.setZoom(subway.value.getZoom() + 1); } catch (e) { console.error('地图缩放时出错:', e); } } }; const zoomOut = () => { if (subway.value) { try { subway.value.setZoom(subway.value.getZoom() - 1); } catch (e) { console.error('地图缩放时出错:', e); } } }; // 获取当前城市的有效站点列表 const getValidStations = (cityName: string): string[] | null => { try { if (!subway.value) { return null; } // 获取所有线路 const lines = subway.value.getLines(); if (!lines || lines.length === 0) { return null; } // 收集所有站点 const stations = new Set<string>(); lines.forEach(line => { if (line.stations && line.stations.length > 0) { line.stations.forEach(station => { stations.add(station.name); }); } }); return Array.from(stations); } catch (e) { console.error('获取站点列表时出错:', e); return null; } }; return { subwayLines, zoomIn, zoomOut }; } }); </script> <style type="text/tailwindcss"> @layer utilities { .subway-line { display: inline-block; width: 12px; height: 2px; margin: 0 4px; vertical-align: middle; } } </style>
<!-- SubWayView.vue --> <template> <div class="font-sans"> <!-- 搜索面板 --> <div v-show="panelVisible" class="fixed top-4 left-1/2 transform -translate-x-1/2 bg-white rounded-xl shadow-lg p-6 max-w-md w-full z-50 transition-all duration-300" > <div class="flex justify-between items-center mb-4"> <h2 class="text-xl font-bold text-gray-900">{{ panelTitle }}</h2> <button @click="closePanel" class="text-gray-500 hover:text-gray-700 focus:outline-none" > <i class="fa fa-times text-lg"></i> </button> </div> <div class="space-y-4"> <!-- 城市选择 --> <div> <label class="block text-sm font-medium text-gray-700 mb-1">城市</label> <div class="relative"> <input v-model="currentCity" @input="handleCityInput" @keypress.enter="changeCity" class="w-full px-4 py-2 border rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="请输入城市名称" /> <div v-show="citySuggestions.length > 0" class="absolute left-0 right-0 top-full mt-1 bg-white rounded-lg shadow-lg z-50" > <div v-for="suggestion in citySuggestions" :key="suggestion" @click="selectCity(suggestion)" class="px-4 py-2 hover:bg-gray-100 cursor-pointer" > {{ suggestion }} </div> </div> </div> <div class="flex space-x-2 mt-2"> <button @click="changeCity" class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg shadow-md" > 切换城市 </button> <button @click="resetToDefault" class="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded-lg" > <i class="fa fa-refresh mr-1"></i> 重置默认 </button> </div> </div> <!-- 站点输入 --> <div> <div class="relative"> <div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"> <i class="fa fa-map-marker text-blue-500"></i> </div> <input v-model="startStation" @keypress.enter="searchRoute" class="w-full pl-10 pr-4 py-2 border rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="请输入起点站" /> <div v-show="isDefaultStartStation" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-xs text-gray-400" > 默认 </div> </div> <div class="relative mt-4"> <div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"> <i class="fa fa-flag text-red-500"></i> </div> <input v-model="endStation" @keypress.enter="searchRoute" class="w-full pl-10 pr-4 py-2 border rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="请输入终点站" /> <div v-show="isDefaultEndStation" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-xs text-gray-400" > 默认 </div> </div> </div> <!-- 查询按钮 --> <button @click="searchRoute" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg shadow-lg mt-4" > 查询路线 </button> <!-- 路线结果 --> <div class="mt-4 bg-gray-100 rounded-lg p-4 text-sm"> <div v-if="loading" class="text-gray-500 animate-pulse"> <i class="fa fa-spinner fa-spin mr-1"></i> {{ loadingMessage }} </div> <div v-else-if="errorMessage" class="text-red-500"> <i class="fa fa-exclamation-circle mr-1"></i> {{ errorMessage }} </div> <div v-else-if="routeResults"> <!-- 路线展示逻辑保持不变 --> <div class="bg-white rounded-lg shadow-sm p-4 mb-4"> <div class="flex justify-between items-center mb-3"> <h3 class="font-medium">{{ startStation }} → {{ endStation }}</h3> <span class="bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full"> <i class="fa fa-clock-o mr-1"></i> 约{{ routeResults.duration || '未知' }}分钟 </span> </div> <!-- 路线步骤展示 --> </div> </div> <div v-else class="text-gray-500"> 请输入起点和终点,点击查询路线 </div> </div> </div> </div> <!-- 显示面板按钮 --> <button v-show="!panelVisible" @click="showPanel" class="fixed top-4 left-4 bg-white hover:bg-gray-100 text-gray-800 font-medium py-2 px-4 rounded-lg shadow-md z-50" > <i class="fa fa-search mr-2"></i> 显示搜索面板 </button> <!-- 百度地铁地图组件 --> <BaiduSubwayMap :currentCity="currentCity" :startStation="startStation" :endStation="endStation" :cityData="cityData" @routeFound="handleRouteFound" @error="handleError" @mapLoaded="handleMapLoaded" /> </div> </template> <script lang="ts"> import { defineComponent, ref, computed, onMounted, watch } from 'vue'; import BaiduSubwayMap from '../components/BaiduSubwayMap.vue'; interface CityData { [city: string]: { start: string; end: string; }; } export default defineComponent({ name: 'SubWayView', components: { BaiduSubwayMap }, setup() { // 状态管理 const currentCity = ref('深圳'); const startStation = ref(''); const endStation = ref(''); const panelVisible = ref(true); const loading = ref(false); const loadingMessage = ref(''); const errorMessage = ref(''); const routeResults = ref(null); const cityData = ref<CityData>({}); const citySuggestions = ref<string[]>([]); const cityHistory = ref<string[]>([]); // 新增:历史记录数组 const panelTitle = ref('深圳地铁线路规划'); // // 计算属性 const isDefaultStartStation = computed(() => { return cityData.value[currentCity.value]?.start === startStation.value; }); const isDefaultEndStation = computed(() => { return cityData.value[currentCity.value]?.end === endStation.value; }); // 生命周期钩子 onMounted(() => { loadCityData(); loadSavedState(); }); // 从city.json加载城市数据 const loadCityData = async () => { try { console.log('开始加载城市数据...'); loading.value = true; loadingMessage.value = '正在加载城市数据...'; const response = await fetch('city.json'); cityData.value = await response.json(); console.log('城市数据加载成功:', cityData.value); // 设置当前城市的默认站点 setDefaultStations(); loading.value = false; } catch (error) { console.error('加载城市数据失败:', error); errorMessage.value = '加载城市数据失败,请稍后再试'; loading.value = false; } }; // 加载保存的状态 const loadSavedState = () => { try { const savedState = localStorage.getItem('subwayMapState'); if (savedState) { const parsedState = JSON.parse(savedState); // 恢复当前城市 if (parsedState.currentCity && cityData.value[parsedState.currentCity]) { currentCity.value = parsedState.currentCity; panelTitle.value = `${currentCity.value}地铁线路规划`; } // 恢复站点 if (parsedState.startStation) { startStation.value = parsedState.startStation; } if (parsedState.endStation) { endStation.value = parsedState.endStation; } // 恢复面板可见性 if (typeof parsedState.panelVisible === 'boolean') { panelVisible.value = parsedState.panelVisible; } console.log('从本地存储恢复状态:', parsedState); } } catch (e) { console.error('恢复应用状态失败:', e); } }; // 保存当前状态到本地存储 const saveState = () => { try { const stateToSave = { currentCity: currentCity.value, startStation: startStation.value, endStation: endStation.value, panelVisible: panelVisible.value }; localStorage.setItem('subwayMapState', JSON.stringify(stateToSave)); } catch (e) { console.error('保存应用状态失败:', e); } }; // 设置当前城市的默认站点 const setDefaultStations = () => { const defaultStations = cityData.value[currentCity.value]; if (defaultStations) { // 只有在站点为空时设置默认值,保留用户修改 if (!startStation.value) { startStation.value = defaultStations.start; } if (!endStation.value) { endStation.value = defaultStations.end; } } }; // 切换城市 const changeCity = () => { console.log(`点击:选择城市...`); const cityName1 = currentCity.value.trim(); console.log(`点击:选择城市${cityName1}`); const defaultStations = cityData.value[currentCity.value]; if (defaultStations) { startStation.value = defaultStations.start; endStation.value = defaultStations.end; panelTitle.value = `${currentCity.value}地铁线路规划`; // 保存状态 saveState(); } // 清除错误消息 errorMessage.value = null; }; // 处理城市输入 const handleCityInput = () => { const query = currentCity.value.trim().toLowerCase(); if (query.length < 2) { citySuggestions.value = []; return; } // 检查输入的城市是否有效(存在于cityData中) const isValidCity = cityData.value[query]; if (isValidCity && !cityHistory.value.includes(query)) { // 添加到历史记录(去重) cityHistory.value.push(query); } // const allCities = [...new Set([...Object.keys(cityData.value), ...cityHistory.value])]; const matchedCities = allCities.filter(city => city.toLowerCase().includes(query) ); // 过滤匹配的城市 //const matchedCities = Object.keys(cityData.value).filter(city => //city.toLowerCase().includes(query) //); // 更新建议列表 citySuggestions.value = matchedCities; }; // 选择城市 const selectCity = (cityName: string) => { currentCity.value = cityName; console.log(`换了地图:选择城市${cityName}`); //setDefaultStations(); // 强制设置默认站点 // itySuggestions.value = []; if (!cityHistory.value.includes(cityName)) { cityHistory.value.push(cityName); } //citySuggestions.value = []; const defaultStations = cityData.value[currentCity.value]; if (defaultStations) { startStation.value = defaultStations.start; endStation.value = defaultStations.end; panelTitle.value = `${currentCity.value}地铁线路规划`; // 保存状态 saveState(); } }; // 搜索路线 const searchRoute = () => { if (!startStation.value || !endStation.value) { errorMessage.value = '请输入起点站和终点站'; return; } // 保存当前状态 saveState(); // 清空错误消息 //errorMessage.value = null; }; // 处理路线结果 const handleRouteFound = (results: any) => { routeResults.value = results; loading.value = false; // 保存当前状态 saveState(); }; // 处理错误 const handleError = (message: string) => { errorMessage.value = message; loading.value = false; }; // 处理地图加载完成 const handleMapLoaded = () => { loading.value = false; }; // 关闭面板 const closePanel = () => { panelVisible.value = false; saveState(); }; // 显示面板 const showPanel = () => { panelVisible.value = true; saveState(); }; // 重置为默认值 const resetToDefault = () => { const defaultStations = cityData.value[currentCity.value]; if (defaultStations) { startStation.value = defaultStations.start; endStation.value = defaultStations.end; panelTitle.value = `${currentCity.value}地铁线路规划`; // 保存状态 saveState(); } }; // 监听面板可见性变化 watch(panelVisible, () => { saveState(); }); // 监听站点变化 watch([startStation, endStation], () => { saveState(); }); return { currentCity, startStation, endStation, panelVisible, loading, loadingMessage, errorMessage, routeResults, cityData, citySuggestions, panelTitle, isDefaultStartStation, isDefaultEndStation, changeCity, handleCityInput, selectCity, searchRoute, closePanel, showPanel, resetToDefault, handleRouteFound, // 确保将方法添加到返回对象中 handleError, handleMapLoaded }; } }); </script> <style scoped> /* 优化字体和间距 */ @tailwind base; @tailwind components; @tailwind utilities; /* 修复搜索面板层级问题 */ .z-50 { z-index: 50; } </style>
哲学管理(学)人生, 文学艺术生活, 自动(计算机学)物理(学)工作, 生物(学)化学逆境, 历史(学)测绘(学)时间, 经济(学)数学金钱(理财), 心理(学)医学情绪, 诗词美容情感, 美学建筑(学)家园, 解构建构(分析)整合学习, 智商情商(IQ、EQ)运筹(学)生存.---Geovin Du(涂聚文)