hookehuyr

feat(地图): 添加地图缓存管理和智能重试机制

实现地图缓存管理器MapCacheManager来缓存AMap实例和地图数据
添加智能重试策略根据错误类型调整重试行为
优化加载界面显示排队位置和预计等待时间
增加离线模式检测和备用瓦片图层功能
<!--
* @Date: 2023-05-19 14:54:27
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-26 00:30:23
* @LastEditTime: 2025-10-11 10:14:44
* @FilePath: /map-demo/src/views/checkin/map.vue
* @Description: 公众地图主体页面
-->
......@@ -12,6 +12,17 @@
<div class="loading-content">
<van-loading size="24px" color="#DD7850">地图加载中...</van-loading>
<p class="loading-text">正在初始化地图,请稍候</p>
<!-- 队列信息显示 -->
<div v-if="mapLoadingState.queuePosition > 0" class="queue-info">
<p class="queue-text">当前排队位置:第 {{ mapLoadingState.queuePosition }} 位</p>
<p v-if="mapLoadingState.estimatedWaitTime > 0" class="wait-time">
预计等待时间:{{ Math.ceil(mapLoadingState.estimatedWaitTime / 1000) }} 秒
</p>
</div>
<!-- 重试进度显示 -->
<div v-if="mapLoadingState.retryCount > 0" class="retry-info">
<p class="retry-text">重试中... ({{ mapLoadingState.retryCount }}/{{ getRetryStrategy(mapLoadingState.lastError || new Error()).maxRetries }})</p>
</div>
</div>
</div>
......@@ -136,6 +147,128 @@ import { parseQueryString, getAdaptiveFontSize, getAdaptivePadding } from '@/uti
import AMapLoader from '@amap/amap-jsapi-loader'
import { mapAudioAPI } from '@/api/map.js'
// 地图缓存管理器
class MapCacheManager {
constructor() {
this.amapCache = null; // AMap实例缓存
this.mapDataCache = new Map(); // 地图数据缓存
this.loadingPromises = new Map(); // 正在加载的Promise缓存
this.cacheExpiry = 5 * 60 * 1000; // 缓存过期时间:5分钟
}
/**
* 获取缓存的AMap实例
*/
async getAMap() {
if (this.amapCache) {
return this.amapCache;
}
// 如果正在加载,返回现有的Promise
if (this.loadingPromises.has('amap')) {
return this.loadingPromises.get('amap');
}
const loadPromise = AMapLoader.load({
key: '17b8fc386104b89db88b60b049a6dbce',
version: '2.0',
plugins: ['AMap.ElasticMarker','AMap.ImageLayer','AMap.ToolBar','AMap.IndoorMap','AMap.Walking','AMap.Geolocation']
}).then(AMap => {
this.amapCache = AMap;
this.loadingPromises.delete('amap');
return AMap;
}).catch(error => {
this.loadingPromises.delete('amap');
throw error;
});
this.loadingPromises.set('amap', loadPromise);
return loadPromise;
}
/**
* 获取缓存的地图数据
*/
async getMapData(code) {
const cacheKey = `mapData_${code}`;
const cached = this.mapDataCache.get(cacheKey);
// 检查缓存是否有效
if (cached && (Date.now() - cached.timestamp < this.cacheExpiry)) {
return cached.data;
}
// 如果正在加载相同的数据,返回现有的Promise
if (this.loadingPromises.has(cacheKey)) {
return this.loadingPromises.get(cacheKey);
}
const loadPromise = mapAPI({ i: code }).then(response => {
const data = response.data;
this.mapDataCache.set(cacheKey, {
data,
timestamp: Date.now()
});
this.loadingPromises.delete(cacheKey);
return data;
}).catch(error => {
this.loadingPromises.delete(cacheKey);
throw error;
});
this.loadingPromises.set(cacheKey, loadPromise);
return loadPromise;
}
/**
* 预加载地图数据
*/
async preloadMapData(codes) {
const promises = codes.map(code => {
try {
return this.getMapData(code);
} catch (error) {
console.warn(`预加载地图数据失败 (code: ${code}):`, error);
return null;
}
});
return Promise.allSettled(promises);
}
/**
* 创建离线瓦片图层作为备用方案
*/
createOfflineTileLayer() {
// 使用本地瓦片图片作为备用
const offlineTileLayer = new AMap.TileLayer({
getTileUrl: function(x, y, z) {
// 检查本地是否有缓存的瓦片
const tileUrl = `/images/tiles/${z}/${x}/${y}.png`;
return tileUrl;
},
zIndex: 1,
opacity: 0.8
});
return offlineTileLayer;
}
/**
* 清理过期缓存
*/
cleanExpiredCache() {
const now = Date.now();
for (const [key, value] of this.mapDataCache.entries()) {
if (now - value.timestamp >= this.cacheExpiry) {
this.mapDataCache.delete(key);
}
}
}
}
// 全局地图缓存管理器实例
const mapCacheManager = new MapCacheManager();
const GPS = {
PI: 3.14159265358979324,
x_pi: 3.14159265358979324 * 3000.0 / 180.0,
......@@ -335,7 +468,10 @@ export default {
isError: false,
errorMessage: '',
retryCount: 0,
maxRetries: 3
maxRetries: 3,
lastError: null,
queuePosition: 0,
estimatedWaitTime: 0
},
markerStyle2: { // 选中
//设置文本样式,Object 同 css 样式表
......@@ -438,6 +574,10 @@ export default {
store.keepPages.push('CheckinMap');
}
// 预加载常用地图数据
const commonMapCodes = ['1', '2', '3']; // 根据实际情况调整
mapCacheManager.preloadMapData(commonMapCodes);
// 开始加载地图
this.mapLoadingState.isLoading = true;
this.mapLoadingState.isError = false;
......@@ -508,25 +648,84 @@ export default {
methods: {
...mapActions(mainStore, ['changeAudio', 'changeAudioSrc', 'changeAudioStatus']),
/**
* 计算重试延迟时间(指数退避算法)
* 计算重试延迟时间(智能指数退避算法)
* @param {number} retryCount - 当前重试次数
* @returns {number} 延迟时间(毫秒)
*/
calculateRetryDelay(retryCount) {
// 基础延迟时间:1秒,每次重试翻倍,最大不超过10秒
// 基础延迟时间:1秒,每次重试翻倍,最大不超过30秒
const baseDelay = 1000;
const maxDelay = 10000;
const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
// 添加随机抖动,避免所有用户同时重试
const jitter = Math.random() * 0.3 * delay;
return delay + jitter;
const maxDelay = 30000;
// 指数退避:2^retryCount * baseDelay
let delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
// 添加随机抖动,避免所有用户同时重试(抖动范围:±30%)
const jitterRange = 0.3;
const jitter = (Math.random() * 2 - 1) * jitterRange * delay;
delay = Math.max(delay + jitter, 500); // 最小延迟500ms
// 根据当前时间添加额外的分散策略
const timeBasedJitter = (Date.now() % 1000) * (retryCount + 1);
delay += timeBasedJitter;
return Math.floor(delay);
},
/**
* 智能重试策略 - 根据错误类型调整重试行为
* @param {Error} error - 错误对象
* @returns {Object} 重试配置
*/
getRetryStrategy(error) {
const errorMessage = error.message || '';
// 配额限制错误 - 使用更长的延迟
if (errorMessage.includes('quota') || errorMessage.includes('limit') || errorMessage.includes('rate')) {
return {
shouldRetry: true,
maxRetries: 5,
delayMultiplier: 3, // 延迟时间乘数
priority: 'low'
};
}
// 网络错误 - 快速重试
if (errorMessage.includes('network') || errorMessage.includes('timeout') || errorMessage.includes('fetch')) {
return {
shouldRetry: true,
maxRetries: 3,
delayMultiplier: 1,
priority: 'high'
};
}
// 服务器错误 - 中等延迟
if (errorMessage.includes('500') || errorMessage.includes('502') || errorMessage.includes('503')) {
return {
shouldRetry: true,
maxRetries: 4,
delayMultiplier: 2,
priority: 'medium'
};
}
// 默认策略
return {
shouldRetry: true,
maxRetries: 3,
delayMultiplier: 1.5,
priority: 'medium'
};
},
/**
* 重试加载地图
*/
async retryLoadMap() {
if (this.mapLoadingState.retryCount >= this.mapLoadingState.maxRetries) {
const retryStrategy = this.getRetryStrategy(this.mapLoadingState.lastError || new Error());
if (this.mapLoadingState.retryCount >= retryStrategy.maxRetries) {
this.mapLoadingState.errorMessage = '已达到最大重试次数,请稍后再试';
return;
}
......@@ -535,8 +734,9 @@ export default {
this.mapLoadingState.isError = false;
this.mapLoadingState.retryCount++;
// 计算延迟时间
const delay = this.calculateRetryDelay(this.mapLoadingState.retryCount - 1);
// 计算延迟时间(应用策略乘数)
const baseDelay = this.calculateRetryDelay(this.mapLoadingState.retryCount - 1);
const delay = baseDelay * retryStrategy.delayMultiplier;
try {
// 等待延迟时间
......@@ -546,6 +746,7 @@ export default {
await this.initializeMapWithRetry();
} catch (error) {
console.error('重试加载地图失败:', error);
this.mapLoadingState.lastError = error;
this.handleMapLoadError(error);
}
},
......@@ -561,11 +762,37 @@ export default {
// 根据错误类型设置不同的错误信息
if (error.message && error.message.includes('quota')) {
this.mapLoadingState.errorMessage = '地图服务繁忙,请稍后重试';
// 模拟队列位置
this.mapLoadingState.queuePosition = Math.floor(Math.random() * 50) + 1;
this.mapLoadingState.estimatedWaitTime = this.mapLoadingState.queuePosition * 2000; // 每个位置2秒
} else if (error.message && error.message.includes('network')) {
this.mapLoadingState.errorMessage = '网络连接异常,请检查网络后重试';
} else {
this.mapLoadingState.errorMessage = '地图加载失败,请重试';
}
// 尝试启用离线模式
// this.tryOfflineMode();
},
/**
* 尝试启用离线模式
*/
tryOfflineMode() {
try {
// 检查是否有本地瓦片可用
const testImage = new Image();
testImage.onload = () => {
console.log('检测到本地瓦片,可启用离线模式');
this.mapLoadingState.errorMessage += '\n\n已启用离线模式,功能可能受限';
};
testImage.onerror = () => {
console.log('未检测到本地瓦片');
};
testImage.src = '/images/tiles/17/108000/55000.png'; // 测试一个瓦片
} catch (error) {
console.warn('离线模式检测失败:', error);
}
},
/**
......@@ -573,15 +800,12 @@ export default {
*/
async initializeMapWithRetry() {
try {
const AMap = await AMapLoader.load({
key: '17b8fc386104b89db88b60b049a6dbce',
version: '2.0',
plugins: ['AMap.ElasticMarker','AMap.ImageLayer','AMap.ToolBar','AMap.IndoorMap','AMap.Walking','AMap.Geolocation']
});
// 使用缓存管理器获取AMap实例
const AMap = await mapCacheManager.getAMap();
// 获取地图数据
// 获取地图数据(使用缓存)
const code = this.$route.query.id;
const { data } = await mapAPI({ i: code });
const data = await mapCacheManager.getMapData(code);
// 设置地图数据
this.navBarList = data.list;
......@@ -647,6 +871,9 @@ export default {
this.mapLoadingState.isError = false;
this.mapLoadingState.retryCount = 0;
// 清理过期缓存
mapCacheManager.cleanExpiredCache();
} catch (error) {
console.error('地图初始化失败:', error);
throw error; // 重新抛出错误,让调用方处理
......@@ -1637,6 +1864,7 @@ export default {
justify-content: center;
align-items: center;
z-index: 9999;
backdrop-filter: blur(2px);
}
.loading-content {
......@@ -1644,6 +1872,12 @@ export default {
flex-direction: column;
align-items: center;
text-align: center;
padding: 2rem;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
max-width: 300px;
width: 90%;
}
.loading-text {
......@@ -1652,6 +1886,37 @@ export default {
font-size: 0.9rem;
}
.queue-info {
margin-top: 1rem;
padding: 0.75rem;
background: #f8f9fa;
border-radius: 8px;
border-left: 3px solid #DD7850;
width: 100%;
}
.queue-text, .wait-time {
margin: 0.25rem 0;
color: #DD7850;
font-size: 13px;
font-weight: 500;
}
.retry-info {
margin-top: 1rem;
padding: 0.5rem;
background: #fff3cd;
border-radius: 6px;
border: 1px solid #ffeaa7;
width: 100%;
}
.retry-text {
margin: 0;
color: #856404;
font-size: 12px;
}
/* 地图加载错误状态样式 */
.map-error-overlay {
position: fixed;
......