|
|
@@ -0,0 +1,715 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
|
|
|
+import AMapLoader from "@amap/amap-jsapi-loader";
|
|
|
+import { Search } from "@element-plus/icons-vue";
|
|
|
+
|
|
|
+interface Props {
|
|
|
+ longitude?: number;
|
|
|
+ latitude?: number;
|
|
|
+ zoom?: number;
|
|
|
+ height?: string;
|
|
|
+ enableClickMark?: boolean;
|
|
|
+ securityJsCode?: string;
|
|
|
+ showSearch?: boolean; // 是否显示地址搜索框
|
|
|
+}
|
|
|
+
|
|
|
+const props = withDefaults(defineProps<Props>(), {
|
|
|
+ zoom: 15,
|
|
|
+ height: "400px",
|
|
|
+ enableClickMark: false,
|
|
|
+ securityJsCode: "",
|
|
|
+ showSearch: false
|
|
|
+});
|
|
|
+
|
|
|
+const emit = defineEmits<{
|
|
|
+ "update:longitude": [value: number];
|
|
|
+ "update:latitude": [value: number];
|
|
|
+ markerMoved: [lng: number, lat: number];
|
|
|
+}>();
|
|
|
+
|
|
|
+const mapContainer = ref<HTMLDivElement>();
|
|
|
+const searchInput = ref("");
|
|
|
+const mapLoading = ref(true);
|
|
|
+const mapError = ref("");
|
|
|
+const loadingProgress = ref(0);
|
|
|
+const loadingStep = ref(0);
|
|
|
+const showFallback = ref(false);
|
|
|
+
|
|
|
+let map: any = null;
|
|
|
+let marker: any = null;
|
|
|
+let geocoder: any = null;
|
|
|
+let placeSearch: any = null; // 使用 PlaceSearch 替代 Geocoder
|
|
|
+
|
|
|
+const hasValidCoords = () => {
|
|
|
+ return (
|
|
|
+ props.longitude !== undefined &&
|
|
|
+ props.latitude !== undefined &&
|
|
|
+ props.longitude !== null &&
|
|
|
+ props.latitude !== null &&
|
|
|
+ props.longitude !== 0 &&
|
|
|
+ props.latitude !== 0
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const updatePosition = (lng: number, lat: number) => {
|
|
|
+ emit("update:longitude", lng);
|
|
|
+ emit("update:latitude", lat);
|
|
|
+ emit("markerMoved", lng, lat);
|
|
|
+};
|
|
|
+
|
|
|
+const addMarker = (lng: number, lat: number) => {
|
|
|
+ if (!map || !AMap) return;
|
|
|
+
|
|
|
+ if (marker) {
|
|
|
+ map.remove(marker);
|
|
|
+ }
|
|
|
+
|
|
|
+ marker = new AMap.Marker({
|
|
|
+ position: [lng, lat],
|
|
|
+ title: "门店位置",
|
|
|
+ draggable: props.enableClickMark,
|
|
|
+ cursor: "move",
|
|
|
+ animation: "AMAP_ANIMATION_DROP"
|
|
|
+ });
|
|
|
+
|
|
|
+ if (props.enableClickMark) {
|
|
|
+ marker.on("dragend", (e: any) => {
|
|
|
+ const lngLat = e.lnglat;
|
|
|
+ updatePosition(lngLat.lng, lngLat.lat);
|
|
|
+ addMarker(lngLat.lng, lngLat.lat);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ map.add(marker);
|
|
|
+ map.setCenter([lng, lat]);
|
|
|
+};
|
|
|
+
|
|
|
+const reloadMap = () => {
|
|
|
+ if (map) {
|
|
|
+ map.destroy();
|
|
|
+ map = null;
|
|
|
+ }
|
|
|
+ showFallback.value = false;
|
|
|
+ initMap();
|
|
|
+};
|
|
|
+
|
|
|
+const showFallbackMap = () => {
|
|
|
+ showFallback.value = true;
|
|
|
+ mapLoading.value = false;
|
|
|
+ mapError.value = '';
|
|
|
+};
|
|
|
+
|
|
|
+const hideFallbackMap = () => {
|
|
|
+ showFallback.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+// 使用 PlaceSearch 进行地址搜索
|
|
|
+const searchAddress = async (address: string) => {
|
|
|
+ console.log('[地址搜索] 开始执行:', address);
|
|
|
+ console.log('[地址搜索] placeSearch:', !!placeSearch);
|
|
|
+ console.log('[地址搜索] AMap:', !!AMap);
|
|
|
+
|
|
|
+ if (!placeSearch || !AMap) {
|
|
|
+ console.error('[地址搜索] PlaceSearch 未初始化');
|
|
|
+ alert('地图组件正在初始化,请稍后再试');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ console.log('[地址搜索] 调用 search 方法...');
|
|
|
+
|
|
|
+ const result = await new Promise((resolve, reject) => {
|
|
|
+ let completed = false;
|
|
|
+
|
|
|
+ const timeoutId = setTimeout(() => {
|
|
|
+ if (!completed) {
|
|
|
+ console.error('[地址搜索] 超时(10 秒)');
|
|
|
+ reject(new Error('地址搜索超时,请检查网络或重试'));
|
|
|
+ }
|
|
|
+ }, 10000);
|
|
|
+
|
|
|
+ placeSearch.search(address, (status: string, result: any) => {
|
|
|
+ completed = true;
|
|
|
+ clearTimeout(timeoutId);
|
|
|
+
|
|
|
+ console.log('[地址搜索] 回调执行:', { status, hasResult: !!result, poiCount: result?.poiList?.count, errorInfo: result?.info });
|
|
|
+
|
|
|
+ if (status === 'complete' && result.poiList && result.poiList.count > 0) {
|
|
|
+ console.log('[地址搜索] 成功,解析结果');
|
|
|
+ resolve(result.poiList.pois[0]); // 返回第一个结果
|
|
|
+ } else {
|
|
|
+ const errorMsg = result?.info || result?.infocode || '未找到该地址';
|
|
|
+ console.error('[地址搜索] 详细错误:', { status, info: result?.info, infocode: result?.infocode });
|
|
|
+ reject(new Error(errorMsg));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ const poi = result as any;
|
|
|
+ const location = poi.location; // POI 对象包含 location 属性
|
|
|
+
|
|
|
+ console.log('[地址搜索] 搜索结果:', location);
|
|
|
+
|
|
|
+ // 更新地图中心点和标记
|
|
|
+ updatePosition(location.lng, location.lat);
|
|
|
+ addMarker(location.lng, location.lat);
|
|
|
+
|
|
|
+ // 设置缩放级别
|
|
|
+ map.setZoomAndCenter(15, [location.lng, location.lat]);
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('[地址搜索] 失败:', error);
|
|
|
+ alert(error.message || '地址搜索失败,请尝试其他地址');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 处理搜索
|
|
|
+const handleSearch = () => {
|
|
|
+ console.log('[搜索] 开始搜索:', searchInput.value);
|
|
|
+ console.log('[搜索] placeSearch 状态:', !!placeSearch);
|
|
|
+ console.log('[搜索] AMap 状态:', !!AMap);
|
|
|
+
|
|
|
+ if (!searchInput.value.trim()) {
|
|
|
+ alert('请输入地址');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保 PlaceSearch 已初始化
|
|
|
+ if (!placeSearch || !AMap) {
|
|
|
+ console.error('[搜索] PlaceSearch 未初始化');
|
|
|
+ alert('地图组件正在初始化,请稍后再试');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('[搜索] 准备调用 searchAddress:', searchInput.value);
|
|
|
+ searchAddress(searchInput.value);
|
|
|
+};
|
|
|
+
|
|
|
+// 按回车搜索
|
|
|
+const handleKeyPress = (e: KeyboardEvent) => {
|
|
|
+ if (e.key === 'Enter') {
|
|
|
+ handleSearch();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 获取当前位置
|
|
|
+const getCurrentLocation = () => {
|
|
|
+ if (navigator.geolocation) {
|
|
|
+ navigator.geolocation.getCurrentPosition(
|
|
|
+ (position) => {
|
|
|
+ const lng = position.coords.longitude;
|
|
|
+ const lat = position.coords.latitude;
|
|
|
+ console.log('获取到当前位置:', lng, lat);
|
|
|
+ updatePosition(lng, lat);
|
|
|
+ addMarker(lng, lat);
|
|
|
+ map.setCenter([lng, lat]);
|
|
|
+ map.setZoom(15);
|
|
|
+ },
|
|
|
+ (error) => {
|
|
|
+ console.warn('获取当前位置失败:', error);
|
|
|
+ // 使用默认位置(北京)
|
|
|
+ const defaultLng = 116.397428;
|
|
|
+ const defaultLat = 39.90923;
|
|
|
+ updatePosition(defaultLng, defaultLat);
|
|
|
+ addMarker(defaultLng, defaultLat);
|
|
|
+ }
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ console.warn('浏览器不支持地理位置');
|
|
|
+ // 使用默认位置
|
|
|
+ const defaultLng = 116.397428;
|
|
|
+ const defaultLat = 39.90923;
|
|
|
+ updatePosition(defaultLng, defaultLat);
|
|
|
+ addMarker(defaultLng, defaultLat);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const initMap = async () => {
|
|
|
+ console.log('[地图初始化] 开始检查...');
|
|
|
+
|
|
|
+ // 等待 DOM 更新
|
|
|
+ await nextTick();
|
|
|
+
|
|
|
+ if (!mapContainer.value) {
|
|
|
+ console.error('[地图初始化] 地图容器不存在');
|
|
|
+ mapError.value = '地图容器未准备好';
|
|
|
+ mapLoading.value = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('[地图初始化] 容器元素:', mapContainer.value);
|
|
|
+ console.log('[地图初始化] 容器高度:', mapContainer.value.style.height);
|
|
|
+ console.log('[地图初始化] 容器 offsetHeight:', mapContainer.value.offsetHeight);
|
|
|
+
|
|
|
+ if (!AMap) {
|
|
|
+ console.error('[地图初始化] AMap 对象未初始化');
|
|
|
+ mapError.value = '地图 SDK 未加载';
|
|
|
+ mapLoading.value = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('[地图初始化] AMap 对象存在:', !!AMap);
|
|
|
+ console.log('[地图初始化] AMap.Map 构造函数:', typeof AMap.Map);
|
|
|
+
|
|
|
+ mapLoading.value = true;
|
|
|
+ mapError.value = "";
|
|
|
+
|
|
|
+ try {
|
|
|
+ const initialCenter = hasValidCoords()
|
|
|
+ ? [props.longitude, props.latitude]
|
|
|
+ : [116.397428, 39.90923];
|
|
|
+
|
|
|
+ console.log('[地图初始化] 开始创建地图实例...');
|
|
|
+ console.log('[地图初始化] 中心点:', initialCenter);
|
|
|
+ console.log('[地图初始化] 缩放级别:', props.zoom);
|
|
|
+
|
|
|
+ // 配置地图选项
|
|
|
+ const mapOptions: any = {
|
|
|
+ zoom: props.zoom,
|
|
|
+ center: initialCenter,
|
|
|
+ resizeEnable: true,
|
|
|
+ viewMode: "2D",
|
|
|
+ keyboardEnable: true,
|
|
|
+ scrollWheel: true,
|
|
|
+ doubleClickZoom: true,
|
|
|
+ zoomEnable: true
|
|
|
+ };
|
|
|
+
|
|
|
+ // 如果 AMap 已加载,添加图层配置
|
|
|
+ if ((window as any).AMap) {
|
|
|
+ mapOptions.layers = [
|
|
|
+ new (window as any).AMap.TileLayer(), // 默认底图
|
|
|
+ new (window as any).AMap.TileLayer.RoadNet() // 路网层
|
|
|
+ ];
|
|
|
+ // 暂时不设置自定义样式,避免 API 错误
|
|
|
+ // mapOptions.mapStyle = 'amap://styles/normal';
|
|
|
+ }
|
|
|
+
|
|
|
+ map = new AMap.Map(mapContainer.value, mapOptions);
|
|
|
+
|
|
|
+ console.log('[地图初始化] 地图实例创建成功:', !!map);
|
|
|
+ console.log('[地图初始化] 地图对象:', map);
|
|
|
+
|
|
|
+ // 先初始化地理编码插件(在地图实例创建之后)
|
|
|
+ console.log('[地图初始化] 开始加载地理编码插件...');
|
|
|
+
|
|
|
+ // 使用 Promise 包装插件加载
|
|
|
+ await new Promise((resolve, reject) => {
|
|
|
+ AMap.plugin(['AMap.PlaceSearch'], () => {
|
|
|
+ console.log('[插件加载] 插件加载回调执行');
|
|
|
+ placeSearch = new AMap.PlaceSearch({
|
|
|
+ pageSize: 5,
|
|
|
+ pageIndex: 1,
|
|
|
+ extensions: 'all' // 返回详细信息
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log('[地图初始化] PlaceSearch 初始化完成');
|
|
|
+ console.log('[地图初始化] placeSearch 对象:', !!placeSearch);
|
|
|
+ console.log('[地图初始化] placeSearch.search:', typeof placeSearch.search);
|
|
|
+ resolve(true);
|
|
|
+ }, (error: any) => {
|
|
|
+ console.error('[插件加载] 失败:', error);
|
|
|
+ reject(error);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ if (hasValidCoords()) {
|
|
|
+ console.log('[地图初始化] 添加标记点');
|
|
|
+ addMarker(props.longitude!, props.latitude!);
|
|
|
+ } else if (props.showSearch) {
|
|
|
+ // 如果是新数据且显示搜索框,获取当前位置
|
|
|
+ console.log('[地图初始化] 无坐标数据,获取当前位置');
|
|
|
+ getCurrentLocation();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (props.enableClickMark) {
|
|
|
+ console.log('[地图初始化] 启用点击事件');
|
|
|
+ map.on("click", (e: any) => {
|
|
|
+ const lng = e.lnglat.getLng();
|
|
|
+ const lat = e.lnglat.getLat();
|
|
|
+ updatePosition(lng, lat);
|
|
|
+ addMarker(lng, lat);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('[地图初始化] 完成');
|
|
|
+ mapLoading.value = false;
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error("[地图初始化] 失败:", error);
|
|
|
+ mapError.value = error.message || "地图加载失败,请检查网络连接";
|
|
|
+ mapLoading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const loadAMap = () => {
|
|
|
+ console.log("开始加载高德地图...");
|
|
|
+
|
|
|
+ // 重置状态
|
|
|
+ loadingProgress.value = 0;
|
|
|
+ loadingStep.value = 0;
|
|
|
+
|
|
|
+ // 设置超时机制
|
|
|
+ const timeoutId = setTimeout(() => {
|
|
|
+ console.warn('地图加载超时');
|
|
|
+ mapError.value = '地图加载超时,请刷新页面重试';
|
|
|
+ mapLoading.value = false;
|
|
|
+ }, 15000); // 15 秒超时
|
|
|
+
|
|
|
+ // 更新进度
|
|
|
+ loadingStep.value = 1;
|
|
|
+ loadingProgress.value = 20;
|
|
|
+
|
|
|
+ // 先检查网络连接
|
|
|
+ fetch('https://webapi.amap.com/maps?v=2.0&key=b1e59efdfdc10271d78db9a556540361')
|
|
|
+ .then(response => {
|
|
|
+ clearTimeout(timeoutId); // 清除超时
|
|
|
+ console.log('高德 API 网络连接测试:', response.status);
|
|
|
+
|
|
|
+ // 更新进度
|
|
|
+ loadingStep.value = 2;
|
|
|
+ loadingProgress.value = 50;
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`网络响应错误:${response.status}`);
|
|
|
+ }
|
|
|
+ return response.text();
|
|
|
+ })
|
|
|
+ .then(data => {
|
|
|
+ console.log('高德 API 响应数据长度:', data.length);
|
|
|
+
|
|
|
+ // 设置安全密钥
|
|
|
+ (window as any)._AMapSecurityConfig = {
|
|
|
+ securityJsCode: 'bcfcaa8d8ef22452d4cf6d3892646262'
|
|
|
+ };
|
|
|
+ console.log('已设置安全密钥');
|
|
|
+
|
|
|
+ // 更新进度
|
|
|
+ loadingStep.value = 3;
|
|
|
+ loadingProgress.value = 80;
|
|
|
+
|
|
|
+ AMapLoader.load({
|
|
|
+ key: "b1e59efdfdc10271d78db9a556540361",
|
|
|
+ version: "2.0",
|
|
|
+ plugins: ["AMap.Scale", "AMap.ToolBar", "AMap.PlaceSearch"]
|
|
|
+ })
|
|
|
+ .then((AMapInstance) => {
|
|
|
+ clearTimeout(timeoutId); // 清除超时
|
|
|
+ console.log("高德地图API加载成功");
|
|
|
+ AMap = AMapInstance;
|
|
|
+ (window as any).AMap = AMapInstance;
|
|
|
+
|
|
|
+ // 完成进度
|
|
|
+ loadingProgress.value = 100;
|
|
|
+
|
|
|
+ initMap();
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ clearTimeout(timeoutId); // 清除超时
|
|
|
+ console.error("加载高德地图失败:", error);
|
|
|
+ mapError.value = `地图加载失败:${error.message || '未知错误'}. 请检查网络连接或 API 密钥是否正确`;
|
|
|
+ mapLoading.value = false;
|
|
|
+ });
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ clearTimeout(timeoutId); // 清除超时
|
|
|
+ console.error('网络连接测试失败:', error);
|
|
|
+ mapError.value = `网络连接测试失败:${error.message}. 请检查网络连接`;
|
|
|
+ mapLoading.value = false;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => [props.longitude, props.latitude],
|
|
|
+ ([newLng, newLat]) => {
|
|
|
+ if (newLng && newLat && map) {
|
|
|
+ addMarker(newLng as number, newLat as number);
|
|
|
+ map.setCenter([newLng, newLat]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ console.log('地图组件挂载,容器存在:', !!mapContainer.value);
|
|
|
+ loadAMap();
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ if (map) {
|
|
|
+ map.destroy();
|
|
|
+ map = null;
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+defineExpose({
|
|
|
+ reloadMap,
|
|
|
+ addMarker,
|
|
|
+ searchAddress // 暴露搜索方法
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="amap-wrapper">
|
|
|
+ <!-- 地址搜索框 -->
|
|
|
+ <div v-if="showSearch" class="search-box">
|
|
|
+ <el-input
|
|
|
+ v-model="searchInput"
|
|
|
+ placeholder="请输入地址进行搜索"
|
|
|
+ clearable
|
|
|
+ @keypress="handleKeyPress"
|
|
|
+ >
|
|
|
+ <template #append>
|
|
|
+ <el-button @click="handleSearch">
|
|
|
+ <el-icon><Search /></el-icon>
|
|
|
+ 搜索
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 地图容器始终渲染 -->
|
|
|
+ <div ref="mapContainer" class="map-container" :style="{ height }"></div>
|
|
|
+
|
|
|
+ <!-- 加载状态覆盖层 -->
|
|
|
+ <div v-if="mapLoading" class="loading-overlay">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ <span>地图加载中...</span>
|
|
|
+ <div class="loading-progress">
|
|
|
+ <div class="progress-bar" :style="{ width: loadingProgress + '%' }"></div>
|
|
|
+ </div>
|
|
|
+ <div class="loading-steps">
|
|
|
+ <div :class="{ 'step-active': loadingStep >= 1 }">🌐 网络检测</div>
|
|
|
+ <div :class="{ 'step-active': loadingStep >= 2 }">🔑 API 验证</div>
|
|
|
+ <div :class="{ 'step-active': loadingStep >= 3 }">🗺️ 地图初始化</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 错误状态覆盖层 -->
|
|
|
+ <div v-else-if="mapError" class="error-overlay">
|
|
|
+ <div class="error-icon">⚠️</div>
|
|
|
+ <span>{{ mapError }}</span>
|
|
|
+ <div class="error-actions">
|
|
|
+ <el-button size="small" @click="reloadMap">重新加载</el-button>
|
|
|
+ <el-button size="small" @click="showFallbackMap">显示备用地图</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-if="hasValidCoords() && !mapLoading && !mapError"
|
|
|
+ class="coord-display"
|
|
|
+ >
|
|
|
+ <span>📍</span>
|
|
|
+ <span>经度:{{ props.longitude?.toFixed(6) }}</span>
|
|
|
+ <span>|</span>
|
|
|
+ <span>纬度:{{ props.latitude?.toFixed(6) }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-else-if="!mapLoading && !mapError" class="coord-empty">
|
|
|
+ <span>📍</span>
|
|
|
+ <span>暂无位置信息,请在地图上点击选择位置或通过地址搜索定位</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 备用静态地图 -->
|
|
|
+ <div v-if="showFallback" class="fallback-map">
|
|
|
+ <div class="fallback-content">
|
|
|
+ <h3>备用地图视图</h3>
|
|
|
+ <div class="fallback-coords">
|
|
|
+ <p><strong>经度:</strong> {{ props.longitude || '未设置' }}</p>
|
|
|
+ <p><strong>纬度:</strong> {{ props.latitude || '未设置' }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="fallback-actions">
|
|
|
+ <el-button @click="hideFallbackMap">关闭备用地图</el-button>
|
|
|
+ <el-button type="primary" @click="reloadMap">重试加载</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.amap-wrapper {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.search-box {
|
|
|
+ margin-bottom: 10px;
|
|
|
+ z-index: 1001;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.map-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ min-height: 300px;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-overlay,
|
|
|
+.error-overlay {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: rgba(255, 255, 255, 0.9);
|
|
|
+ z-index: 1000;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-state,
|
|
|
+.error-state,
|
|
|
+.coord-display,
|
|
|
+.coord-empty {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 12px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-top: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.error-state {
|
|
|
+ flex-direction: column;
|
|
|
+ color: #f56c6c;
|
|
|
+ padding: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-state {
|
|
|
+ color: #409eff;
|
|
|
+ text-align: center;
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-spinner {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border: 4px solid #f3f3f3;
|
|
|
+ border-top: 4px solid #409eff;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ margin: 0 auto 15px;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+
|
|
|
+.loading-progress {
|
|
|
+ width: 80%;
|
|
|
+ height: 6px;
|
|
|
+ background-color: #e4e7ed;
|
|
|
+ border-radius: 3px;
|
|
|
+ margin: 15px auto;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-bar {
|
|
|
+ height: 100%;
|
|
|
+ background-color: #409eff;
|
|
|
+ transition: width 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-steps {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-around;
|
|
|
+ margin-top: 15px;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-steps div {
|
|
|
+ opacity: 0.5;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.step-active {
|
|
|
+ opacity: 1;
|
|
|
+ color: #409eff;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.error-icon {
|
|
|
+ font-size: 32px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.error-actions {
|
|
|
+ margin-top: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.error-actions .el-button {
|
|
|
+ margin: 0 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.fallback-map {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 1000;
|
|
|
+}
|
|
|
+
|
|
|
+.fallback-content {
|
|
|
+ background: white;
|
|
|
+ padding: 30px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
+ text-align: center;
|
|
|
+ max-width: 400px;
|
|
|
+}
|
|
|
+
|
|
|
+.fallback-content h3 {
|
|
|
+ margin-top: 0;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.fallback-coords {
|
|
|
+ text-align: left;
|
|
|
+ margin: 20px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.fallback-coords p {
|
|
|
+ margin: 10px 0;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.fallback-actions {
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.fallback-actions .el-button {
|
|
|
+ margin: 0 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.coord-display {
|
|
|
+ color: #606266;
|
|
|
+ font-size: 13px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-top: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.coord-empty {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 12px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-top: 8px;
|
|
|
+}
|
|
|
+</style>
|