hookehuyr

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

- 新增 Pinia store 管理全局 loading 状态
- 在 axios 拦截器中实现请求计数逻辑
- 添加全屏 loading 蒙版组件
- 支持并发请求完成检测和错误自动关闭
......@@ -120,6 +120,25 @@ API 请求基于 Axios 封装,配置文件位于 `src/utils/request.js`,支
- 错误处理
- Token 自动携带
## 全局加载机制(低网速优化)
-`src/stores/loading.js` 新增 Pinia 全局 loading store,使用并发计数管理加载状态。
-`src/utils/axios.js` 请求/响应拦截器接入该 store:请求开始计数 +1、响应成功计数 -1、任一错误发生时重置计数并关闭加载。
-`src/App.vue` 添加全屏蒙版与 `van-loading` 组件,居中展示“加载中...”动画,直至页面所有接口完成或出现错误。
- 行为说明:
- 并发多个接口时仅在全部完成后关闭蒙版;
- 任一接口失败立即关闭蒙版,错误提示交由业务层处理(如 `api/fn.js` 中的 `showFailToast`)。
### 相关文件
- `src/stores/loading.js`:全局 loading 状态管理。
- `src/utils/axios.js`:拦截器计数与异常兜底。
- `src/App.vue`:全屏 loading 蒙版展示。
### 优化建议
- 如需对某些非页面关键接口(如埋点、心跳)排除 loading,可在拦截器中根据 `config.url` 做白名单过滤。
## 组件说明
### 页面组件
......
<!--
* @Date: 2025-10-30 10:28:42
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-11-04 21:09:39
* @FilePath: /stdj_h5/src/App.vue
* @Description: 文件描述
-->
<template>
<div id="app">
<router-view />
</div>
<div id="app">
<router-view />
<!-- 全局loading蒙版 -->
<div v-if="is_loading" class="global-loading-overlay">
<van-loading type="spinner" size="24px" color="#FFF" text-color="#FFF">加载中...</van-loading>
</div>
</div>
</template>
<script setup>
// 这里可以添加全局逻辑
import { computed } from 'vue'
import { useLoadingStore } from '@/stores/loading.js'
// 使用Pinia全局loading状态
const loading_store = useLoadingStore()
const is_loading = computed(() => loading_store.is_loading)
</script>
<style>
/* 全局样式已在 style.css 中定义 */
</style>
\ No newline at end of file
<style lang="less">
// 全局样式已在 style.css 中定义
.global-loading-overlay {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100vh;
background: rgba(0, 0, 0, 0.35);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
</style>
......
/*
* @Date: 2025-11-04 10:45:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-11-04 10:45:00
* @FilePath: /src/stores/loading.js
* @Description: 全局Loading状态管理
*/
import { defineStore } from 'pinia'
/**
* 全局loading状态管理
* @description 使用计数的方式管理并发请求的loading显示与隐藏
*/
export const useLoadingStore = defineStore('global_loading', {
state: () => ({
// 当前正在进行中的网络请求数量
pending_count: 0,
// 是否显示全屏loading
is_loading: false
}),
getters: {
// 是否存在进行中的请求
has_pending(state) {
return state.pending_count > 0
}
},
actions: {
/**
* 开始一次请求,增加计数并显示loading
* @returns {void}
*/
start() {
this.pending_count += 1
this.is_loading = true
},
/**
* 结束一次请求,减少计数,当计数归零时隐藏loading
* @returns {void}
*/
end() {
if (this.pending_count > 0) {
this.pending_count -= 1
}
if (this.pending_count === 0) {
this.is_loading = false
}
},
/**
* 请求异常或需要强制关闭时,重置计数并隐藏loading
* @returns {void}
*/
reset() {
this.pending_count = 0
this.is_loading = false
}
}
})
\ No newline at end of file
......@@ -7,6 +7,7 @@
* @Description:
*/
import axios from 'axios';
import { useLoadingStore } from '@/stores/loading.js'
// import router from '@/router';
// import qs from 'Qs'
// import { strExist } from '@/utils/tools'
......@@ -17,10 +18,33 @@ axios.defaults.params = {
};
/**
* 安全调用全局loading store
* @param {string} method 方法名:'start' | 'end' | 'reset'
* @returns {void}
*/
function safe_loading(method) {
try {
const loading = useLoadingStore()
if (method === 'start') {
loading.start()
} else if (method === 'end') {
loading.end()
} else if (method === 'reset') {
loading.reset()
}
} catch (e) {
// 兜底:Pinia未就绪或Store未初始化时跳过,并输出调试信息
console.debug('全局loading未初始化,已跳过:', e)
}
}
/**
* @description 请求拦截器
*/
axios.interceptors.request.use(
config => {
// 开始全局loading计数(安全封装)
safe_loading('start')
// const url_params = parseQueryString(location.href);
// GET请求默认打上时间戳,避免从缓存中拿数据。
const timestamp = config.method === 'get' ? (new Date()).valueOf() : '';
......@@ -35,6 +59,8 @@ axios.interceptors.request.use(
},
error => {
// 请求错误处理
// 出现错误时重置loading,避免长时间遮挡(安全封装)
safe_loading('reset')
return Promise.reject(error);
});
......@@ -43,9 +69,15 @@ axios.interceptors.request.use(
*/
axios.interceptors.response.use(
response => {
// 响应完成后减少计数
// 正常响应结束时减少pending计数,归零后关闭蒙版(安全封装)
safe_loading('end')
return response;
},
error => {
// 响应异常时直接重置隐藏loading
// 任一接口失败则关闭全局loading,交由页面/业务自行反馈错误(安全封装)
safe_loading('reset')
return Promise.reject(error);
});
......