项目结构:
<!--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>
输出: