hookehuyr

feat(loading): 新增全局加载机制优化低网速体验

- 新增 Pinia store 管理全局 loading 状态
- 在 axios 拦截器中实现请求计数逻辑
- 添加全屏 loading 蒙版组件
- 支持并发请求完成检测和错误自动关闭
...@@ -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>
......
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
......