hookehuyr

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

实现地图缓存管理器MapCacheManager来缓存AMap实例和地图数据
添加智能重试策略根据错误类型调整重试行为
优化加载界面显示排队位置和预计等待时间
增加离线模式检测和备用瓦片图层功能
1 <!-- 1 <!--
2 * @Date: 2023-05-19 14:54:27 2 * @Date: 2023-05-19 14:54:27
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-09-26 00:30:23 4 + * @LastEditTime: 2025-10-11 10:14:44
5 * @FilePath: /map-demo/src/views/checkin/map.vue 5 * @FilePath: /map-demo/src/views/checkin/map.vue
6 * @Description: 公众地图主体页面 6 * @Description: 公众地图主体页面
7 --> 7 -->
...@@ -12,6 +12,17 @@ ...@@ -12,6 +12,17 @@
12 <div class="loading-content"> 12 <div class="loading-content">
13 <van-loading size="24px" color="#DD7850">地图加载中...</van-loading> 13 <van-loading size="24px" color="#DD7850">地图加载中...</van-loading>
14 <p class="loading-text">正在初始化地图,请稍候</p> 14 <p class="loading-text">正在初始化地图,请稍候</p>
15 + <!-- 队列信息显示 -->
16 + <div v-if="mapLoadingState.queuePosition > 0" class="queue-info">
17 + <p class="queue-text">当前排队位置:第 {{ mapLoadingState.queuePosition }} 位</p>
18 + <p v-if="mapLoadingState.estimatedWaitTime > 0" class="wait-time">
19 + 预计等待时间:{{ Math.ceil(mapLoadingState.estimatedWaitTime / 1000) }} 秒
20 + </p>
21 + </div>
22 + <!-- 重试进度显示 -->
23 + <div v-if="mapLoadingState.retryCount > 0" class="retry-info">
24 + <p class="retry-text">重试中... ({{ mapLoadingState.retryCount }}/{{ getRetryStrategy(mapLoadingState.lastError || new Error()).maxRetries }})</p>
25 + </div>
15 </div> 26 </div>
16 </div> 27 </div>
17 28
...@@ -136,6 +147,128 @@ import { parseQueryString, getAdaptiveFontSize, getAdaptivePadding } from '@/uti ...@@ -136,6 +147,128 @@ import { parseQueryString, getAdaptiveFontSize, getAdaptivePadding } from '@/uti
136 import AMapLoader from '@amap/amap-jsapi-loader' 147 import AMapLoader from '@amap/amap-jsapi-loader'
137 import { mapAudioAPI } from '@/api/map.js' 148 import { mapAudioAPI } from '@/api/map.js'
138 149
150 +// 地图缓存管理器
151 +class MapCacheManager {
152 + constructor() {
153 + this.amapCache = null; // AMap实例缓存
154 + this.mapDataCache = new Map(); // 地图数据缓存
155 + this.loadingPromises = new Map(); // 正在加载的Promise缓存
156 + this.cacheExpiry = 5 * 60 * 1000; // 缓存过期时间:5分钟
157 + }
158 +
159 + /**
160 + * 获取缓存的AMap实例
161 + */
162 + async getAMap() {
163 + if (this.amapCache) {
164 + return this.amapCache;
165 + }
166 +
167 + // 如果正在加载,返回现有的Promise
168 + if (this.loadingPromises.has('amap')) {
169 + return this.loadingPromises.get('amap');
170 + }
171 +
172 + const loadPromise = AMapLoader.load({
173 + key: '17b8fc386104b89db88b60b049a6dbce',
174 + version: '2.0',
175 + plugins: ['AMap.ElasticMarker','AMap.ImageLayer','AMap.ToolBar','AMap.IndoorMap','AMap.Walking','AMap.Geolocation']
176 + }).then(AMap => {
177 + this.amapCache = AMap;
178 + this.loadingPromises.delete('amap');
179 + return AMap;
180 + }).catch(error => {
181 + this.loadingPromises.delete('amap');
182 + throw error;
183 + });
184 +
185 + this.loadingPromises.set('amap', loadPromise);
186 + return loadPromise;
187 + }
188 +
189 + /**
190 + * 获取缓存的地图数据
191 + */
192 + async getMapData(code) {
193 + const cacheKey = `mapData_${code}`;
194 + const cached = this.mapDataCache.get(cacheKey);
195 +
196 + // 检查缓存是否有效
197 + if (cached && (Date.now() - cached.timestamp < this.cacheExpiry)) {
198 + return cached.data;
199 + }
200 +
201 + // 如果正在加载相同的数据,返回现有的Promise
202 + if (this.loadingPromises.has(cacheKey)) {
203 + return this.loadingPromises.get(cacheKey);
204 + }
205 +
206 + const loadPromise = mapAPI({ i: code }).then(response => {
207 + const data = response.data;
208 + this.mapDataCache.set(cacheKey, {
209 + data,
210 + timestamp: Date.now()
211 + });
212 + this.loadingPromises.delete(cacheKey);
213 + return data;
214 + }).catch(error => {
215 + this.loadingPromises.delete(cacheKey);
216 + throw error;
217 + });
218 +
219 + this.loadingPromises.set(cacheKey, loadPromise);
220 + return loadPromise;
221 + }
222 +
223 + /**
224 + * 预加载地图数据
225 + */
226 + async preloadMapData(codes) {
227 + const promises = codes.map(code => {
228 + try {
229 + return this.getMapData(code);
230 + } catch (error) {
231 + console.warn(`预加载地图数据失败 (code: ${code}):`, error);
232 + return null;
233 + }
234 + });
235 +
236 + return Promise.allSettled(promises);
237 + }
238 +
239 + /**
240 + * 创建离线瓦片图层作为备用方案
241 + */
242 + createOfflineTileLayer() {
243 + // 使用本地瓦片图片作为备用
244 + const offlineTileLayer = new AMap.TileLayer({
245 + getTileUrl: function(x, y, z) {
246 + // 检查本地是否有缓存的瓦片
247 + const tileUrl = `/images/tiles/${z}/${x}/${y}.png`;
248 + return tileUrl;
249 + },
250 + zIndex: 1,
251 + opacity: 0.8
252 + });
253 + return offlineTileLayer;
254 + }
255 +
256 + /**
257 + * 清理过期缓存
258 + */
259 + cleanExpiredCache() {
260 + const now = Date.now();
261 + for (const [key, value] of this.mapDataCache.entries()) {
262 + if (now - value.timestamp >= this.cacheExpiry) {
263 + this.mapDataCache.delete(key);
264 + }
265 + }
266 + }
267 +}
268 +
269 +// 全局地图缓存管理器实例
270 +const mapCacheManager = new MapCacheManager();
271 +
139 const GPS = { 272 const GPS = {
140 PI: 3.14159265358979324, 273 PI: 3.14159265358979324,
141 x_pi: 3.14159265358979324 * 3000.0 / 180.0, 274 x_pi: 3.14159265358979324 * 3000.0 / 180.0,
...@@ -335,7 +468,10 @@ export default { ...@@ -335,7 +468,10 @@ export default {
335 isError: false, 468 isError: false,
336 errorMessage: '', 469 errorMessage: '',
337 retryCount: 0, 470 retryCount: 0,
338 - maxRetries: 3 471 + maxRetries: 3,
472 + lastError: null,
473 + queuePosition: 0,
474 + estimatedWaitTime: 0
339 }, 475 },
340 markerStyle2: { // 选中 476 markerStyle2: { // 选中
341 //设置文本样式,Object 同 css 样式表 477 //设置文本样式,Object 同 css 样式表
...@@ -438,6 +574,10 @@ export default { ...@@ -438,6 +574,10 @@ export default {
438 store.keepPages.push('CheckinMap'); 574 store.keepPages.push('CheckinMap');
439 } 575 }
440 576
577 + // 预加载常用地图数据
578 + const commonMapCodes = ['1', '2', '3']; // 根据实际情况调整
579 + mapCacheManager.preloadMapData(commonMapCodes);
580 +
441 // 开始加载地图 581 // 开始加载地图
442 this.mapLoadingState.isLoading = true; 582 this.mapLoadingState.isLoading = true;
443 this.mapLoadingState.isError = false; 583 this.mapLoadingState.isError = false;
...@@ -508,25 +648,84 @@ export default { ...@@ -508,25 +648,84 @@ export default {
508 methods: { 648 methods: {
509 ...mapActions(mainStore, ['changeAudio', 'changeAudioSrc', 'changeAudioStatus']), 649 ...mapActions(mainStore, ['changeAudio', 'changeAudioSrc', 'changeAudioStatus']),
510 /** 650 /**
511 - * 计算重试延迟时间(指数退避算法) 651 + * 计算重试延迟时间(智能指数退避算法)
512 * @param {number} retryCount - 当前重试次数 652 * @param {number} retryCount - 当前重试次数
513 * @returns {number} 延迟时间(毫秒) 653 * @returns {number} 延迟时间(毫秒)
514 */ 654 */
515 calculateRetryDelay(retryCount) { 655 calculateRetryDelay(retryCount) {
516 - // 基础延迟时间:1秒,每次重试翻倍,最大不超过10秒 656 + // 基础延迟时间:1秒,每次重试翻倍,最大不超过30秒
517 const baseDelay = 1000; 657 const baseDelay = 1000;
518 - const maxDelay = 10000; 658 + const maxDelay = 30000;
519 - const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay); 659 +
520 - // 添加随机抖动,避免所有用户同时重试 660 + // 指数退避:2^retryCount * baseDelay
521 - const jitter = Math.random() * 0.3 * delay; 661 + let delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
522 - return delay + jitter; 662 +
663 + // 添加随机抖动,避免所有用户同时重试(抖动范围:±30%)
664 + const jitterRange = 0.3;
665 + const jitter = (Math.random() * 2 - 1) * jitterRange * delay;
666 + delay = Math.max(delay + jitter, 500); // 最小延迟500ms
667 +
668 + // 根据当前时间添加额外的分散策略
669 + const timeBasedJitter = (Date.now() % 1000) * (retryCount + 1);
670 + delay += timeBasedJitter;
671 +
672 + return Math.floor(delay);
673 + },
674 +
675 + /**
676 + * 智能重试策略 - 根据错误类型调整重试行为
677 + * @param {Error} error - 错误对象
678 + * @returns {Object} 重试配置
679 + */
680 + getRetryStrategy(error) {
681 + const errorMessage = error.message || '';
682 +
683 + // 配额限制错误 - 使用更长的延迟
684 + if (errorMessage.includes('quota') || errorMessage.includes('limit') || errorMessage.includes('rate')) {
685 + return {
686 + shouldRetry: true,
687 + maxRetries: 5,
688 + delayMultiplier: 3, // 延迟时间乘数
689 + priority: 'low'
690 + };
691 + }
692 +
693 + // 网络错误 - 快速重试
694 + if (errorMessage.includes('network') || errorMessage.includes('timeout') || errorMessage.includes('fetch')) {
695 + return {
696 + shouldRetry: true,
697 + maxRetries: 3,
698 + delayMultiplier: 1,
699 + priority: 'high'
700 + };
701 + }
702 +
703 + // 服务器错误 - 中等延迟
704 + if (errorMessage.includes('500') || errorMessage.includes('502') || errorMessage.includes('503')) {
705 + return {
706 + shouldRetry: true,
707 + maxRetries: 4,
708 + delayMultiplier: 2,
709 + priority: 'medium'
710 + };
711 + }
712 +
713 + // 默认策略
714 + return {
715 + shouldRetry: true,
716 + maxRetries: 3,
717 + delayMultiplier: 1.5,
718 + priority: 'medium'
719 + };
523 }, 720 },
524 721
525 /** 722 /**
526 * 重试加载地图 723 * 重试加载地图
527 */ 724 */
528 async retryLoadMap() { 725 async retryLoadMap() {
529 - if (this.mapLoadingState.retryCount >= this.mapLoadingState.maxRetries) { 726 + const retryStrategy = this.getRetryStrategy(this.mapLoadingState.lastError || new Error());
727 +
728 + if (this.mapLoadingState.retryCount >= retryStrategy.maxRetries) {
530 this.mapLoadingState.errorMessage = '已达到最大重试次数,请稍后再试'; 729 this.mapLoadingState.errorMessage = '已达到最大重试次数,请稍后再试';
531 return; 730 return;
532 } 731 }
...@@ -535,8 +734,9 @@ export default { ...@@ -535,8 +734,9 @@ export default {
535 this.mapLoadingState.isError = false; 734 this.mapLoadingState.isError = false;
536 this.mapLoadingState.retryCount++; 735 this.mapLoadingState.retryCount++;
537 736
538 - // 计算延迟时间 737 + // 计算延迟时间(应用策略乘数)
539 - const delay = this.calculateRetryDelay(this.mapLoadingState.retryCount - 1); 738 + const baseDelay = this.calculateRetryDelay(this.mapLoadingState.retryCount - 1);
739 + const delay = baseDelay * retryStrategy.delayMultiplier;
540 740
541 try { 741 try {
542 // 等待延迟时间 742 // 等待延迟时间
...@@ -546,6 +746,7 @@ export default { ...@@ -546,6 +746,7 @@ export default {
546 await this.initializeMapWithRetry(); 746 await this.initializeMapWithRetry();
547 } catch (error) { 747 } catch (error) {
548 console.error('重试加载地图失败:', error); 748 console.error('重试加载地图失败:', error);
749 + this.mapLoadingState.lastError = error;
549 this.handleMapLoadError(error); 750 this.handleMapLoadError(error);
550 } 751 }
551 }, 752 },
...@@ -561,11 +762,37 @@ export default { ...@@ -561,11 +762,37 @@ export default {
561 // 根据错误类型设置不同的错误信息 762 // 根据错误类型设置不同的错误信息
562 if (error.message && error.message.includes('quota')) { 763 if (error.message && error.message.includes('quota')) {
563 this.mapLoadingState.errorMessage = '地图服务繁忙,请稍后重试'; 764 this.mapLoadingState.errorMessage = '地图服务繁忙,请稍后重试';
765 + // 模拟队列位置
766 + this.mapLoadingState.queuePosition = Math.floor(Math.random() * 50) + 1;
767 + this.mapLoadingState.estimatedWaitTime = this.mapLoadingState.queuePosition * 2000; // 每个位置2秒
564 } else if (error.message && error.message.includes('network')) { 768 } else if (error.message && error.message.includes('network')) {
565 this.mapLoadingState.errorMessage = '网络连接异常,请检查网络后重试'; 769 this.mapLoadingState.errorMessage = '网络连接异常,请检查网络后重试';
566 } else { 770 } else {
567 this.mapLoadingState.errorMessage = '地图加载失败,请重试'; 771 this.mapLoadingState.errorMessage = '地图加载失败,请重试';
568 } 772 }
773 +
774 + // 尝试启用离线模式
775 + // this.tryOfflineMode();
776 + },
777 +
778 + /**
779 + * 尝试启用离线模式
780 + */
781 + tryOfflineMode() {
782 + try {
783 + // 检查是否有本地瓦片可用
784 + const testImage = new Image();
785 + testImage.onload = () => {
786 + console.log('检测到本地瓦片,可启用离线模式');
787 + this.mapLoadingState.errorMessage += '\n\n已启用离线模式,功能可能受限';
788 + };
789 + testImage.onerror = () => {
790 + console.log('未检测到本地瓦片');
791 + };
792 + testImage.src = '/images/tiles/17/108000/55000.png'; // 测试一个瓦片
793 + } catch (error) {
794 + console.warn('离线模式检测失败:', error);
795 + }
569 }, 796 },
570 797
571 /** 798 /**
...@@ -573,15 +800,12 @@ export default { ...@@ -573,15 +800,12 @@ export default {
573 */ 800 */
574 async initializeMapWithRetry() { 801 async initializeMapWithRetry() {
575 try { 802 try {
576 - const AMap = await AMapLoader.load({ 803 + // 使用缓存管理器获取AMap实例
577 - key: '17b8fc386104b89db88b60b049a6dbce', 804 + const AMap = await mapCacheManager.getAMap();
578 - version: '2.0',
579 - plugins: ['AMap.ElasticMarker','AMap.ImageLayer','AMap.ToolBar','AMap.IndoorMap','AMap.Walking','AMap.Geolocation']
580 - });
581 805
582 - // 获取地图数据 806 + // 获取地图数据(使用缓存)
583 const code = this.$route.query.id; 807 const code = this.$route.query.id;
584 - const { data } = await mapAPI({ i: code }); 808 + const data = await mapCacheManager.getMapData(code);
585 809
586 // 设置地图数据 810 // 设置地图数据
587 this.navBarList = data.list; 811 this.navBarList = data.list;
...@@ -647,6 +871,9 @@ export default { ...@@ -647,6 +871,9 @@ export default {
647 this.mapLoadingState.isError = false; 871 this.mapLoadingState.isError = false;
648 this.mapLoadingState.retryCount = 0; 872 this.mapLoadingState.retryCount = 0;
649 873
874 + // 清理过期缓存
875 + mapCacheManager.cleanExpiredCache();
876 +
650 } catch (error) { 877 } catch (error) {
651 console.error('地图初始化失败:', error); 878 console.error('地图初始化失败:', error);
652 throw error; // 重新抛出错误,让调用方处理 879 throw error; // 重新抛出错误,让调用方处理
...@@ -1637,6 +1864,7 @@ export default { ...@@ -1637,6 +1864,7 @@ export default {
1637 justify-content: center; 1864 justify-content: center;
1638 align-items: center; 1865 align-items: center;
1639 z-index: 9999; 1866 z-index: 9999;
1867 + backdrop-filter: blur(2px);
1640 } 1868 }
1641 1869
1642 .loading-content { 1870 .loading-content {
...@@ -1644,6 +1872,12 @@ export default { ...@@ -1644,6 +1872,12 @@ export default {
1644 flex-direction: column; 1872 flex-direction: column;
1645 align-items: center; 1873 align-items: center;
1646 text-align: center; 1874 text-align: center;
1875 + padding: 2rem;
1876 + background: white;
1877 + border-radius: 12px;
1878 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
1879 + max-width: 300px;
1880 + width: 90%;
1647 } 1881 }
1648 1882
1649 .loading-text { 1883 .loading-text {
...@@ -1652,6 +1886,37 @@ export default { ...@@ -1652,6 +1886,37 @@ export default {
1652 font-size: 0.9rem; 1886 font-size: 0.9rem;
1653 } 1887 }
1654 1888
1889 +.queue-info {
1890 + margin-top: 1rem;
1891 + padding: 0.75rem;
1892 + background: #f8f9fa;
1893 + border-radius: 8px;
1894 + border-left: 3px solid #DD7850;
1895 + width: 100%;
1896 +}
1897 +
1898 +.queue-text, .wait-time {
1899 + margin: 0.25rem 0;
1900 + color: #DD7850;
1901 + font-size: 13px;
1902 + font-weight: 500;
1903 +}
1904 +
1905 +.retry-info {
1906 + margin-top: 1rem;
1907 + padding: 0.5rem;
1908 + background: #fff3cd;
1909 + border-radius: 6px;
1910 + border: 1px solid #ffeaa7;
1911 + width: 100%;
1912 +}
1913 +
1914 +.retry-text {
1915 + margin: 0;
1916 + color: #856404;
1917 + font-size: 12px;
1918 +}
1919 +
1655 /* 地图加载错误状态样式 */ 1920 /* 地图加载错误状态样式 */
1656 .map-error-overlay { 1921 .map-error-overlay {
1657 position: fixed; 1922 position: fixed;
......