feat(loading): 新增全局加载机制优化低网速体验
- 新增 Pinia store 管理全局 loading 状态 - 在 axios 拦截器中实现请求计数逻辑 - 添加全屏 loading 蒙版组件 - 支持并发请求完成检测和错误自动关闭
Showing
4 changed files
with
143 additions
and
3 deletions
| ... | @@ -120,6 +120,25 @@ API 请求基于 Axios 封装,配置文件位于 `src/utils/request.js`,支 | ... | @@ -120,6 +120,25 @@ API 请求基于 Axios 封装,配置文件位于 `src/utils/request.js`,支 |
| 120 | - 错误处理 | 120 | - 错误处理 |
| 121 | - Token 自动携带 | 121 | - Token 自动携带 |
| 122 | 122 | ||
| 123 | +## 全局加载机制(低网速优化) | ||
| 124 | + | ||
| 125 | +- 在 `src/stores/loading.js` 新增 Pinia 全局 loading store,使用并发计数管理加载状态。 | ||
| 126 | +- 在 `src/utils/axios.js` 请求/响应拦截器接入该 store:请求开始计数 +1、响应成功计数 -1、任一错误发生时重置计数并关闭加载。 | ||
| 127 | +- 在 `src/App.vue` 添加全屏蒙版与 `van-loading` 组件,居中展示“加载中...”动画,直至页面所有接口完成或出现错误。 | ||
| 128 | +- 行为说明: | ||
| 129 | + - 并发多个接口时仅在全部完成后关闭蒙版; | ||
| 130 | + - 任一接口失败立即关闭蒙版,错误提示交由业务层处理(如 `api/fn.js` 中的 `showFailToast`)。 | ||
| 131 | + | ||
| 132 | +### 相关文件 | ||
| 133 | + | ||
| 134 | +- `src/stores/loading.js`:全局 loading 状态管理。 | ||
| 135 | +- `src/utils/axios.js`:拦截器计数与异常兜底。 | ||
| 136 | +- `src/App.vue`:全屏 loading 蒙版展示。 | ||
| 137 | + | ||
| 138 | +### 优化建议 | ||
| 139 | + | ||
| 140 | +- 如需对某些非页面关键接口(如埋点、心跳)排除 loading,可在拦截器中根据 `config.url` 做白名单过滤。 | ||
| 141 | + | ||
| 123 | ## 组件说明 | 142 | ## 组件说明 |
| 124 | 143 | ||
| 125 | ### 页面组件 | 144 | ### 页面组件 | ... | ... |
| 1 | +<!-- | ||
| 2 | + * @Date: 2025-10-30 10:28:42 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-11-04 21:09:39 | ||
| 5 | + * @FilePath: /stdj_h5/src/App.vue | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | +--> | ||
| 1 | <template> | 8 | <template> |
| 2 | <div id="app"> | 9 | <div id="app"> |
| 3 | <router-view /> | 10 | <router-view /> |
| 11 | + <!-- 全局loading蒙版 --> | ||
| 12 | + <div v-if="is_loading" class="global-loading-overlay"> | ||
| 13 | + <van-loading type="spinner" size="24px" color="#FFF" text-color="#FFF">加载中...</van-loading> | ||
| 14 | + </div> | ||
| 4 | </div> | 15 | </div> |
| 5 | </template> | 16 | </template> |
| 6 | 17 | ||
| 7 | <script setup> | 18 | <script setup> |
| 8 | -// 这里可以添加全局逻辑 | 19 | +import { computed } from 'vue' |
| 20 | +import { useLoadingStore } from '@/stores/loading.js' | ||
| 21 | + | ||
| 22 | +// 使用Pinia全局loading状态 | ||
| 23 | +const loading_store = useLoadingStore() | ||
| 24 | +const is_loading = computed(() => loading_store.is_loading) | ||
| 9 | </script> | 25 | </script> |
| 10 | 26 | ||
| 11 | -<style> | 27 | +<style lang="less"> |
| 12 | -/* 全局样式已在 style.css 中定义 */ | 28 | +// 全局样式已在 style.css 中定义 |
| 29 | +.global-loading-overlay { | ||
| 30 | + position: fixed; | ||
| 31 | + left: 0; | ||
| 32 | + top: 0; | ||
| 33 | + width: 100%; | ||
| 34 | + height: 100vh; | ||
| 35 | + background: rgba(0, 0, 0, 0.35); | ||
| 36 | + display: flex; | ||
| 37 | + align-items: center; | ||
| 38 | + justify-content: center; | ||
| 39 | + z-index: 9999; | ||
| 40 | +} | ||
| 13 | </style> | 41 | </style> | ... | ... |
src/stores/loading.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2025-11-04 10:45:00 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-11-04 10:45:00 | ||
| 5 | + * @FilePath: /src/stores/loading.js | ||
| 6 | + * @Description: 全局Loading状态管理 | ||
| 7 | + */ | ||
| 8 | +import { defineStore } from 'pinia' | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 全局loading状态管理 | ||
| 12 | + * @description 使用计数的方式管理并发请求的loading显示与隐藏 | ||
| 13 | + */ | ||
| 14 | +export const useLoadingStore = defineStore('global_loading', { | ||
| 15 | + state: () => ({ | ||
| 16 | + // 当前正在进行中的网络请求数量 | ||
| 17 | + pending_count: 0, | ||
| 18 | + // 是否显示全屏loading | ||
| 19 | + is_loading: false | ||
| 20 | + }), | ||
| 21 | + | ||
| 22 | + getters: { | ||
| 23 | + // 是否存在进行中的请求 | ||
| 24 | + has_pending(state) { | ||
| 25 | + return state.pending_count > 0 | ||
| 26 | + } | ||
| 27 | + }, | ||
| 28 | + | ||
| 29 | + actions: { | ||
| 30 | + /** | ||
| 31 | + * 开始一次请求,增加计数并显示loading | ||
| 32 | + * @returns {void} | ||
| 33 | + */ | ||
| 34 | + start() { | ||
| 35 | + this.pending_count += 1 | ||
| 36 | + this.is_loading = true | ||
| 37 | + }, | ||
| 38 | + | ||
| 39 | + /** | ||
| 40 | + * 结束一次请求,减少计数,当计数归零时隐藏loading | ||
| 41 | + * @returns {void} | ||
| 42 | + */ | ||
| 43 | + end() { | ||
| 44 | + if (this.pending_count > 0) { | ||
| 45 | + this.pending_count -= 1 | ||
| 46 | + } | ||
| 47 | + if (this.pending_count === 0) { | ||
| 48 | + this.is_loading = false | ||
| 49 | + } | ||
| 50 | + }, | ||
| 51 | + | ||
| 52 | + /** | ||
| 53 | + * 请求异常或需要强制关闭时,重置计数并隐藏loading | ||
| 54 | + * @returns {void} | ||
| 55 | + */ | ||
| 56 | + reset() { | ||
| 57 | + this.pending_count = 0 | ||
| 58 | + this.is_loading = false | ||
| 59 | + } | ||
| 60 | + } | ||
| 61 | +}) | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -7,6 +7,7 @@ | ... | @@ -7,6 +7,7 @@ |
| 7 | * @Description: | 7 | * @Description: |
| 8 | */ | 8 | */ |
| 9 | import axios from 'axios'; | 9 | import axios from 'axios'; |
| 10 | +import { useLoadingStore } from '@/stores/loading.js' | ||
| 10 | // import router from '@/router'; | 11 | // import router from '@/router'; |
| 11 | // import qs from 'Qs' | 12 | // import qs from 'Qs' |
| 12 | // import { strExist } from '@/utils/tools' | 13 | // import { strExist } from '@/utils/tools' |
| ... | @@ -17,10 +18,33 @@ axios.defaults.params = { | ... | @@ -17,10 +18,33 @@ axios.defaults.params = { |
| 17 | }; | 18 | }; |
| 18 | 19 | ||
| 19 | /** | 20 | /** |
| 21 | + * 安全调用全局loading store | ||
| 22 | + * @param {string} method 方法名:'start' | 'end' | 'reset' | ||
| 23 | + * @returns {void} | ||
| 24 | + */ | ||
| 25 | +function safe_loading(method) { | ||
| 26 | + try { | ||
| 27 | + const loading = useLoadingStore() | ||
| 28 | + if (method === 'start') { | ||
| 29 | + loading.start() | ||
| 30 | + } else if (method === 'end') { | ||
| 31 | + loading.end() | ||
| 32 | + } else if (method === 'reset') { | ||
| 33 | + loading.reset() | ||
| 34 | + } | ||
| 35 | + } catch (e) { | ||
| 36 | + // 兜底:Pinia未就绪或Store未初始化时跳过,并输出调试信息 | ||
| 37 | + console.debug('全局loading未初始化,已跳过:', e) | ||
| 38 | + } | ||
| 39 | +} | ||
| 40 | + | ||
| 41 | +/** | ||
| 20 | * @description 请求拦截器 | 42 | * @description 请求拦截器 |
| 21 | */ | 43 | */ |
| 22 | axios.interceptors.request.use( | 44 | axios.interceptors.request.use( |
| 23 | config => { | 45 | config => { |
| 46 | + // 开始全局loading计数(安全封装) | ||
| 47 | + safe_loading('start') | ||
| 24 | // const url_params = parseQueryString(location.href); | 48 | // const url_params = parseQueryString(location.href); |
| 25 | // GET请求默认打上时间戳,避免从缓存中拿数据。 | 49 | // GET请求默认打上时间戳,避免从缓存中拿数据。 |
| 26 | const timestamp = config.method === 'get' ? (new Date()).valueOf() : ''; | 50 | const timestamp = config.method === 'get' ? (new Date()).valueOf() : ''; |
| ... | @@ -35,6 +59,8 @@ axios.interceptors.request.use( | ... | @@ -35,6 +59,8 @@ axios.interceptors.request.use( |
| 35 | }, | 59 | }, |
| 36 | error => { | 60 | error => { |
| 37 | // 请求错误处理 | 61 | // 请求错误处理 |
| 62 | + // 出现错误时重置loading,避免长时间遮挡(安全封装) | ||
| 63 | + safe_loading('reset') | ||
| 38 | return Promise.reject(error); | 64 | return Promise.reject(error); |
| 39 | }); | 65 | }); |
| 40 | 66 | ||
| ... | @@ -43,9 +69,15 @@ axios.interceptors.request.use( | ... | @@ -43,9 +69,15 @@ axios.interceptors.request.use( |
| 43 | */ | 69 | */ |
| 44 | axios.interceptors.response.use( | 70 | axios.interceptors.response.use( |
| 45 | response => { | 71 | response => { |
| 72 | + // 响应完成后减少计数 | ||
| 73 | + // 正常响应结束时减少pending计数,归零后关闭蒙版(安全封装) | ||
| 74 | + safe_loading('end') | ||
| 46 | return response; | 75 | return response; |
| 47 | }, | 76 | }, |
| 48 | error => { | 77 | error => { |
| 78 | + // 响应异常时直接重置隐藏loading | ||
| 79 | + // 任一接口失败则关闭全局loading,交由页面/业务自行反馈错误(安全封装) | ||
| 80 | + safe_loading('reset') | ||
| 49 | return Promise.reject(error); | 81 | return Promise.reject(error); |
| 50 | }); | 82 | }); |
| 51 | 83 | ... | ... |
-
Please register or login to post a comment