项目结构:
<!--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'; //,PropTypeinterface 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"><!-- 搜索面板 --><divv-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"><inputv-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="请输入城市名称"/><divv-show="citySuggestions.length > 0"class="absolute left-0 right-0 top-full mt-1 bg-white rounded-lg shadow-lg z-50"><divv-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><inputv-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="请输入起点站"/><divv-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><inputv-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="请输入终点站"/><divv-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><!-- 显示面板按钮 --><buttonv-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>
输出: