hookehuyr

feat(map): 添加地图加载状态管理和错误处理功能

重构地图组件,增加加载状态管理和错误处理机制:
1. 添加加载中和错误状态UI展示
2. 实现带指数退避的重试机制
3. 优化错误信息提示
4. 清理未使用的组件导入
1 { 1 {
2 "globals": { 2 "globals": {
3 "EffectScope": true, 3 "EffectScope": true,
4 - "ElMessage": true,
5 "computed": true, 4 "computed": true,
6 "createApp": true, 5 "createApp": true,
7 "customRef": true, 6 "customRef": true,
......
...@@ -13,10 +13,6 @@ declare module '@vue/runtime-core' { ...@@ -13,10 +13,6 @@ declare module '@vue/runtime-core' {
13 AudioBackground1: typeof import('./src/components/audioBackground1.vue')['default'] 13 AudioBackground1: typeof import('./src/components/audioBackground1.vue')['default']
14 AudioList: typeof import('./src/components/audioList.vue')['default'] 14 AudioList: typeof import('./src/components/audioList.vue')['default']
15 BottomNav: typeof import('./src/components/BottomNav.vue')['default'] 15 BottomNav: typeof import('./src/components/BottomNav.vue')['default']
16 - ElButton: typeof import('element-plus/es')['ElButton']
17 - ElInput: typeof import('element-plus/es')['ElInput']
18 - ElOption: typeof import('element-plus/es')['ElOption']
19 - ElSelect: typeof import('element-plus/es')['ElSelect']
20 Floor: typeof import('./src/components/Floor/index.vue')['default'] 16 Floor: typeof import('./src/components/Floor/index.vue')['default']
21 InfoPopup: typeof import('./src/components/InfoPopup.vue')['default'] 17 InfoPopup: typeof import('./src/components/InfoPopup.vue')['default']
22 InfoPopupLite: typeof import('./src/components/InfoPopupLite.vue')['default'] 18 InfoPopupLite: typeof import('./src/components/InfoPopupLite.vue')['default']
...@@ -28,25 +24,9 @@ declare module '@vue/runtime-core' { ...@@ -28,25 +24,9 @@ declare module '@vue/runtime-core' {
28 RouterLink: typeof import('vue-router')['RouterLink'] 24 RouterLink: typeof import('vue-router')['RouterLink']
29 RouterView: typeof import('vue-router')['RouterView'] 25 RouterView: typeof import('vue-router')['RouterView']
30 SvgIcon: typeof import('./src/components/Floor/svgIcon.vue')['default'] 26 SvgIcon: typeof import('./src/components/Floor/svgIcon.vue')['default']
31 - VanBackTop: typeof import('vant/es')['BackTop']
32 - VanButton: typeof import('vant/es')['Button']
33 - VanCol: typeof import('vant/es')['Col']
34 - VanConfigProvider: typeof import('vant/es')['ConfigProvider']
35 VanDialog: typeof import('vant/es')['Dialog'] 27 VanDialog: typeof import('vant/es')['Dialog']
36 - VanField: typeof import('vant/es')['Field']
37 - VanFloatingPanel: typeof import('vant/es')['FloatingPanel']
38 VanIcon: typeof import('vant/es')['Icon'] 28 VanIcon: typeof import('vant/es')['Icon']
39 - VanImage: typeof import('vant/es')['Image']
40 - VanImagePreview: typeof import('vant/es')['ImagePreview']
41 - VanOverlay: typeof import('vant/es')['Overlay']
42 VanPopup: typeof import('vant/es')['Popup'] 29 VanPopup: typeof import('vant/es')['Popup']
43 - VanRow: typeof import('vant/es')['Row']
44 - VanSlider: typeof import('vant/es')['Slider']
45 - VanSwipe: typeof import('vant/es')['Swipe']
46 - VanSwipeItem: typeof import('vant/es')['SwipeItem']
47 - VanTab: typeof import('vant/es')['Tab']
48 - VanTabs: typeof import('vant/es')['Tabs']
49 - VanToast: typeof import('vant/es')['Toast']
50 VRViewer: typeof import('./src/components/VRViewer/index.vue')['default'] 30 VRViewer: typeof import('./src/components/VRViewer/index.vue')['default']
51 } 31 }
52 } 32 }
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
2 export {} 2 export {}
3 declare global { 3 declare global {
4 const EffectScope: typeof import('vue')['EffectScope'] 4 const EffectScope: typeof import('vue')['EffectScope']
5 - const ElMessage: typeof import('element-plus/es')['ElMessage']
6 const computed: typeof import('vue')['computed'] 5 const computed: typeof import('vue')['computed']
7 const createApp: typeof import('vue')['createApp'] 6 const createApp: typeof import('vue')['createApp']
8 const customRef: typeof import('vue')['customRef'] 7 const customRef: typeof import('vue')['customRef']
......
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-21 19:03:12 4 + * @LastEditTime: 2025-09-25 21:54:49
5 * @FilePath: /map-demo/src/views/checkin/map.vue 5 * @FilePath: /map-demo/src/views/checkin/map.vue
6 * @Description: 公众地图主体页面 6 * @Description: 公众地图主体页面
7 --> 7 -->
8 <template> 8 <template>
9 <div ref="root" :style="{ height: mapHeight, position: 'relative', overflow: 'hidden' }"> 9 <div ref="root" :style="{ height: mapHeight, position: 'relative', overflow: 'hidden' }">
10 + <!-- 地图加载状态提示 -->
11 + <div v-if="mapLoadingState.isLoading" class="map-loading-overlay">
12 + <div class="loading-content">
13 + <van-loading size="24px" color="#DD7850">地图加载中...</van-loading>
14 + <p class="loading-text">正在初始化地图,请稍候</p>
15 + </div>
16 + </div>
17 +
18 + <!-- 地图加载错误提示 -->
19 + <div v-if="mapLoadingState.isError" class="map-error-overlay">
20 + <div class="error-content">
21 + <van-icon name="warning-o" size="48px" color="#ff4444" />
22 + <p class="error-title">地图加载失败</p>
23 + <p class="error-message">{{ mapLoadingState.errorMessage }}</p>
24 + <van-button
25 + type="primary"
26 + color="#DD7850"
27 + @click="retryLoadMap"
28 + :loading="mapLoadingState.isLoading"
29 + >
30 + 重新加载
31 + </van-button>
32 + </div>
33 + </div>
34 +
10 <div id="container"></div> 35 <div id="container"></div>
11 <!-- 添加导航面板容器 --> 36 <!-- 添加导航面板容器 -->
12 <div id="walking-panel" style="position: absolute; bottom: 1rem; left: 1rem; padding: 1rem;"></div> 37 <div id="walking-panel" style="position: absolute; bottom: 1rem; left: 1rem; padding: 1rem;"></div>
...@@ -304,6 +329,14 @@ export default { ...@@ -304,6 +329,14 @@ export default {
304 floatingPanelBorderRadius: '1.25rem' 329 floatingPanelBorderRadius: '1.25rem'
305 }, 330 },
306 showClose: false, 331 showClose: false,
332 + // 地图加载状态管理
333 + mapLoadingState: {
334 + isLoading: true,
335 + isError: false,
336 + errorMessage: '',
337 + retryCount: 0,
338 + maxRetries: 3
339 + },
307 markerStyle2: { // 选中 340 markerStyle2: { // 选中
308 //设置文本样式,Object 同 css 样式表 341 //设置文本样式,Object 同 css 样式表
309 "padding": ".5rem .2rem .5rem .2rem", 342 "padding": ".5rem .2rem .5rem .2rem",
...@@ -405,102 +438,44 @@ export default { ...@@ -405,102 +438,44 @@ export default {
405 store.keepPages.push('CheckinMap'); 438 store.keepPages.push('CheckinMap');
406 } 439 }
407 440
408 - const AMap = await AMapLoader.load({ 441 + // 开始加载地图
409 - key: '17b8fc386104b89db88b60b049a6dbce', // 控制台获取 442 + this.mapLoadingState.isLoading = true;
410 - version: '2.0', // 指定API版本 443 + this.mapLoadingState.isError = false;
411 - plugins: ['AMap.ElasticMarker','AMap.ImageLayer','AMap.ToolBar','AMap.IndoorMap','AMap.Walking','AMap.Geolocation'] // 必须加载步行导航插件 444 +
412 - }) 445 + try {
413 - const code = this.$route.query.id; 446 + await this.initializeMapWithRetry();
414 - const { data } = await mapAPI({ i: code }); 447 + } catch (error) {
415 - this.navBarList = data.list; // 底部导航条 448 + console.error('地图初始化失败:', error);
416 - this.mapTiles = data.level; // 获取图层 449 + this.handleMapLoadError(error);
417 - this.navKey = data.list.length ? data.list[0]['id'] : 0; // 默认选中 第一个 id
418 - this.navList = data.list.length ? data.list.filter(item => item.id === this.navKey)[0]['list'] : []; // 返回默认选中项的实体信息
419 - this.data_center = data.map.center.map(item => Number(item)); // 地图中心点
420 - this.data_zoom = data.map.zoom; // 地图默认缩放
421 - this.data_rotation = data.map.rotation; // 地图旋转角度
422 - this.data_zooms = data.map.zooms.map(item => Number(item)); // 地图默认缩放范围
423 - this.data_paths = data.map.path ? data.map.path : {}; // 地图默认导航路径
424 - this.data_logo = data.map.map_logo ? data.map.map_logo : ''; // 地图logo
425 - this.point_range = data.map.map_range ? data.map.map_range : []; // 地图定位范围
426 - if (data.map.map_layers) { // 地图默认图层
427 - if (data.map.map_layers === 'satellite') { // 卫星和路网
428 - this.data_layers = [new AMap.TileLayer.Satellite(), new AMap.TileLayer.RoadNet()]
429 - } else { // 平面图
430 - this.data_layers = [];
431 - }
432 - }
433 - if (data.map.path) {
434 - for (const key in data.map.path) {
435 - const element = data.map.path[key];
436 - this.data_path_list.push({
437 - name: key,
438 - path: element,
439 - status: true
440 - })
441 - }
442 - }
443 - // 地图标题
444 - document.title = data.map.map_title;
445 - // 微信分享
446 - const shareData = {
447 - title: data.map.map_title, // 分享标题
448 - desc: '别院地图', // 分享描述
449 - link: location.origin + location.pathname + location.hash, // 分享链接,该链接域名或路径必须与当前页面对应的公众号 JS 安全域名一致
450 - imgUrl: '', // 分享图标
451 - success: function () {
452 - // console.warn('设置成功');
453 - }
454 } 450 }
455 - // 分享好友(微信好友或qq好友)
456 - wx.updateAppMessageShareData(shareData);
457 - // 分享到朋友圈或qq空间
458 - wx.updateTimelineShareData(shareData);
459 - // 分享到腾讯微博
460 - wx.onMenuShareWeibo(shareData);
461 - // 初始化地图
462 - this.initMap();
463 - // this.setMapBoundary();
464 - // 使用之前获取当前地址,判断当前是否能够获取经纬度
465 - // wx.getLocation({
466 - // type: 'wgs84', // 默认为wgs84的gps坐标,如果要返回直接给openLocation用的火星坐标,可传入'gcj02'
467 - // success: (res) => {
468 - // var latitude = res.latitude; // 纬度,浮点数,范围为90 ~ -90
469 - // var longitude = res.longitude; // 经度,浮点数,范围为180 ~ -180。
470 - // var speed = res.speed; // 速度,以米/每秒计
471 - // var accuracy = res.accuracy; // 位置精度
472 - // this.current_lng = GPS.gcj_encrypt(latitude, longitude).lon;
473 - // this.current_lat = GPS.gcj_encrypt(latitude, longitude).lat;
474 - // },
475 - // });
476 - // 设置贴片地图
477 - this.setTitleLayer();
478 - // 地图标题
479 - document.title = data.map.map_title;
480 - //
481 - // setTimeout(() => {
482 - // this.info_height = (0.5 * window.innerHeight);
483 - // // 浮动面板样式
484 - // $('.van-floating-panel__content').css('borderRadius', '1.5rem');
485 - // }, 2000);
486 - // 初始化步行导航插件时指定面板容器
487 - this.walking = new AMap.Walking({
488 - map: this.map,
489 - // panel: 'walking-panel', // 必须指定存在的DOM元素ID
490 - hideMarkers: false, // 设置隐藏路径规划的起始点图标
491 - isOutline: true, // 使用map属性时,绘制的规划线路是否显示描边
492 - autoFitView: true, // 是否自动调整地图视野到显示的路线
493 - });
494 }, 451 },
495 // keep-alive 组件激活时调用 452 // keep-alive 组件激活时调用
496 activated() { 453 activated() {
497 - // 组件被激活时,不需要重新初始化地图,保持之前的状态 454 + // 组件被激活时,检查地图是否已经初始化
498 - console.log('地图组件已激活,保持之前状态'); 455 + console.log('地图组件已激活');
456 +
499 // 确保当前页面在缓存列表中 457 // 确保当前页面在缓存列表中
500 const store = mainStore(); 458 const store = mainStore();
501 if (!store.keepPages.includes('CheckinMap')) { 459 if (!store.keepPages.includes('CheckinMap')) {
502 store.keepPages.push('CheckinMap'); 460 store.keepPages.push('CheckinMap');
503 } 461 }
462 +
463 + // 如果地图还未初始化或加载失败,重新尝试加载
464 + if (!this.map || this.mapLoadingState.isError) {
465 + console.log('地图未初始化或加载失败,重新加载');
466 + this.mapLoadingState.isLoading = true;
467 + this.mapLoadingState.isError = false;
468 + this.initializeMapWithRetry().catch(error => {
469 + console.error('重新激活时地图加载失败:', error);
470 + this.handleMapLoadError(error);
471 + });
472 + } else {
473 + // 地图已经初始化,重新设置地图大小以适应容器
474 + this.$nextTick(() => {
475 + this.map.getSize();
476 + });
477 + }
478 +
504 // 重置page-info组件状态,关闭浮动面板 479 // 重置page-info组件状态,关闭浮动面板
505 this.info_height = 0; 480 this.info_height = 0;
506 this.itemInfo = {}; 481 this.itemInfo = {};
...@@ -511,12 +486,6 @@ export default { ...@@ -511,12 +486,6 @@ export default {
511 // 还原标记点样式 486 // 还原标记点样式
512 this.resetMarkStyle(); 487 this.resetMarkStyle();
513 }); 488 });
514 - // 如果地图已经初始化,重新设置地图大小以适应容器
515 - if (this.map) {
516 - this.$nextTick(() => {
517 - this.map.getSize();
518 - });
519 - }
520 }, 489 },
521 // keep-alive 组件停用时调用 490 // keep-alive 组件停用时调用
522 deactivated() { 491 deactivated() {
...@@ -539,6 +508,151 @@ export default { ...@@ -539,6 +508,151 @@ export default {
539 methods: { 508 methods: {
540 ...mapActions(mainStore, ['changeAudio', 'changeAudioSrc', 'changeAudioStatus']), 509 ...mapActions(mainStore, ['changeAudio', 'changeAudioSrc', 'changeAudioStatus']),
541 /** 510 /**
511 + * 计算重试延迟时间(指数退避算法)
512 + * @param {number} retryCount - 当前重试次数
513 + * @returns {number} 延迟时间(毫秒)
514 + */
515 + calculateRetryDelay(retryCount) {
516 + // 基础延迟时间:1秒,每次重试翻倍,最大不超过10秒
517 + const baseDelay = 1000;
518 + const maxDelay = 10000;
519 + const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
520 + // 添加随机抖动,避免所有用户同时重试
521 + const jitter = Math.random() * 0.3 * delay;
522 + return delay + jitter;
523 + },
524 +
525 + /**
526 + * 重试加载地图
527 + */
528 + async retryLoadMap() {
529 + if (this.mapLoadingState.retryCount >= this.mapLoadingState.maxRetries) {
530 + this.mapLoadingState.errorMessage = '已达到最大重试次数,请稍后再试';
531 + return;
532 + }
533 +
534 + this.mapLoadingState.isLoading = true;
535 + this.mapLoadingState.isError = false;
536 + this.mapLoadingState.retryCount++;
537 +
538 + // 计算延迟时间
539 + const delay = this.calculateRetryDelay(this.mapLoadingState.retryCount - 1);
540 +
541 + try {
542 + // 等待延迟时间
543 + await new Promise(resolve => setTimeout(resolve, delay));
544 +
545 + // 重新初始化地图
546 + await this.initializeMapWithRetry();
547 + } catch (error) {
548 + console.error('重试加载地图失败:', error);
549 + this.handleMapLoadError(error);
550 + }
551 + },
552 +
553 + /**
554 + * 处理地图加载错误
555 + * @param {Error} error - 错误对象
556 + */
557 + handleMapLoadError(error) {
558 + this.mapLoadingState.isLoading = false;
559 + this.mapLoadingState.isError = true;
560 +
561 + // 根据错误类型设置不同的错误信息
562 + if (error.message && error.message.includes('quota')) {
563 + this.mapLoadingState.errorMessage = '地图服务繁忙,请稍后重试';
564 + } else if (error.message && error.message.includes('network')) {
565 + this.mapLoadingState.errorMessage = '网络连接异常,请检查网络后重试';
566 + } else {
567 + this.mapLoadingState.errorMessage = '地图加载失败,请重试';
568 + }
569 + },
570 +
571 + /**
572 + * 带重试机制的地图初始化
573 + */
574 + async initializeMapWithRetry() {
575 + try {
576 + const AMap = await AMapLoader.load({
577 + key: '17b8fc386104b89db88b60b049a6dbce',
578 + version: '2.0',
579 + plugins: ['AMap.ElasticMarker','AMap.ImageLayer','AMap.ToolBar','AMap.IndoorMap','AMap.Walking','AMap.Geolocation']
580 + });
581 +
582 + // 获取地图数据
583 + const code = this.$route.query.id;
584 + const { data } = await mapAPI({ i: code });
585 +
586 + // 设置地图数据
587 + this.navBarList = data.list;
588 + this.mapTiles = data.level;
589 + this.navKey = data.list.length ? data.list[0]['id'] : 0;
590 + this.navList = data.list.length ? data.list.filter(item => item.id === this.navKey)[0]['list'] : [];
591 + this.data_center = data.map.center.map(item => Number(item));
592 + this.data_zoom = data.map.zoom;
593 + this.data_rotation = data.map.rotation;
594 + this.data_zooms = data.map.zooms.map(item => Number(item));
595 + this.data_paths = data.map.path ? data.map.path : {};
596 + this.data_logo = data.map.map_logo ? data.map.map_logo : '';
597 + this.point_range = data.map.map_range ? data.map.map_range : [];
598 +
599 + if (data.map.map_layers) {
600 + if (data.map.map_layers === 'satellite') {
601 + this.data_layers = [new AMap.TileLayer.Satellite(), new AMap.TileLayer.RoadNet()];
602 + } else {
603 + this.data_layers = [];
604 + }
605 + }
606 +
607 + if (data.map.path) {
608 + for (const key in data.map.path) {
609 + const element = data.map.path[key];
610 + this.data_path_list.push({
611 + name: key,
612 + path: element,
613 + status: true
614 + });
615 + }
616 + }
617 +
618 + // 设置页面标题
619 + document.title = data.map.map_title;
620 +
621 + // 微信分享配置
622 + const shareData = {
623 + title: data.map.map_title,
624 + desc: '别院地图',
625 + link: location.origin + location.pathname + location.hash,
626 + imgUrl: '',
627 + success: function () {}
628 + };
629 + wx.updateAppMessageShareData(shareData);
630 + wx.updateTimelineShareData(shareData);
631 + wx.onMenuShareWeibo(shareData);
632 +
633 + // 初始化地图
634 + this.initMap();
635 + this.setTitleLayer();
636 +
637 + // 初始化步行导航
638 + this.walking = new AMap.Walking({
639 + map: this.map,
640 + hideMarkers: false,
641 + isOutline: true,
642 + autoFitView: true,
643 + });
644 +
645 + // 地图加载成功
646 + this.mapLoadingState.isLoading = false;
647 + this.mapLoadingState.isError = false;
648 + this.mapLoadingState.retryCount = 0;
649 +
650 + } catch (error) {
651 + console.error('地图初始化失败:', error);
652 + throw error; // 重新抛出错误,让调用方处理
653 + }
654 + },
655 + /**
542 * 创建标记点的HTML内容,包含图标和文字 656 * 创建标记点的HTML内容,包含图标和文字
543 * @param {Object} entityInfo - 实体信息 657 * @param {Object} entityInfo - 实体信息
544 * @param {String} textDirection - 文字方向 ('vertical' 或 'horizontal') 658 * @param {String} textDirection - 文字方向 ('vertical' 或 'horizontal')
...@@ -1510,4 +1624,68 @@ export default { ...@@ -1510,4 +1624,68 @@ export default {
1510 .van-floating-panel__header-bar { 1624 .van-floating-panel__header-bar {
1511 background: none; 1625 background: none;
1512 } 1626 }
1627 +
1628 +/* 地图加载状态样式 */
1629 +.map-loading-overlay {
1630 + position: fixed;
1631 + top: 0;
1632 + left: 0;
1633 + width: 100%;
1634 + height: 100%;
1635 + background-color: rgba(255, 255, 255, 0.9);
1636 + display: flex;
1637 + justify-content: center;
1638 + align-items: center;
1639 + z-index: 9999;
1640 +}
1641 +
1642 +.loading-content {
1643 + display: flex;
1644 + flex-direction: column;
1645 + align-items: center;
1646 + text-align: center;
1647 +}
1648 +
1649 +.loading-text {
1650 + margin-top: 1rem;
1651 + color: #666;
1652 + font-size: 0.9rem;
1653 +}
1654 +
1655 +/* 地图加载错误状态样式 */
1656 +.map-error-overlay {
1657 + position: fixed;
1658 + top: 0;
1659 + left: 0;
1660 + width: 100%;
1661 + height: 100%;
1662 + background-color: rgba(255, 255, 255, 0.95);
1663 + display: flex;
1664 + justify-content: center;
1665 + align-items: center;
1666 + z-index: 9999;
1667 +}
1668 +
1669 +.error-content {
1670 + display: flex;
1671 + flex-direction: column;
1672 + align-items: center;
1673 + text-align: center;
1674 + padding: 2rem;
1675 + max-width: 300px;
1676 +}
1677 +
1678 +.error-title {
1679 + margin: 1rem 0 0.5rem 0;
1680 + font-size: 1.1rem;
1681 + font-weight: 600;
1682 + color: #333;
1683 +}
1684 +
1685 +.error-message {
1686 + margin: 0.5rem 0 1.5rem 0;
1687 + color: #666;
1688 + font-size: 0.9rem;
1689 + line-height: 1.4;
1690 +}
1513 </style> 1691 </style>
......