diff --git a/project_codebase.md b/project_codebase.md index c7298dd..c7049d8 100644 --- a/project_codebase.md +++ b/project_codebase.md @@ -8,6 +8,11 @@ - **Implementation**: Programmatically mounts `WMessage.vue`. - **New Feature**: Supports `position` argument: `'top-center' | 'top-left' | 'top-right' | 'bottom-center' | 'bottom-left' | 'bottom-right'`. +### `src/utils/loading.ts` +- **Purpose**: Provides a global interface for showing a full-screen blocking loading overlay. +- **Methods**: `show()`, `hide()`. +- **Implementation**: Programmatically mounts `WLoading.vue` with a reference counter to handle concurrent requests. + ### `src/service/request/index.ts` - **Purpose**: Encapsulates Axios for API requests. - **Features**: @@ -15,7 +20,9 @@ - Adds `Authorization: Bearer `. - Adds `X-App-Sign` and `X-App-Timestamp` headers. - Starts `NProgress`. + - **New**: Shows blocking loading overlay if `config.showLoading` is true. - Response Interceptor: Unwraps `data`; handles global errors (401, 403, 500); prioritizes backend error messages; stops `NProgress`. + - **New**: Hides blocking loading overlay if `config.showLoading` is true. - Config: `showLoading`, `showError`. ### `src/service/api/auth.ts` diff --git a/project_doing.md b/project_doing.md index d2a9d6d..d1f9479 100644 --- a/project_doing.md +++ b/project_doing.md @@ -41,3 +41,11 @@ - **Scope**: - `src/utils/message.ts` (Update logic to support multiple containers) - `src/components/ui/WMessage.vue` (No changes needed, styles handled by container) + +### [Task 4] Implement Fullscreen Loading +- **Time**: 2025-12-18 +- **Goal**: Add blocking loading overlay controlled by request config. +- **Scope**: + - `src/components/ui/WLoading.vue` (Create) + - `src/utils/loading.ts` (Create) + - `src/service/request/index.ts` (Update interceptors) diff --git a/project_index.md b/project_index.md index 62022df..fc288d1 100644 --- a/project_index.md +++ b/project_index.md @@ -1,11 +1,13 @@ # Project File Index - `src/utils/message.ts`: Global message utility. +- `src/utils/loading.ts`: Global loading utility (blocking overlay). - `src/service/request/index.ts`: Axios wrapper. - `src/service/api/auth.ts`: Authentication API. - `src/service/api/score.ts`: Score API. - `src/stores/user.ts`: User Pinia store. - `src/stores/score.ts`: Score Pinia store. - `src/components/ui/WMessage.vue`: Message component UI. +- `src/components/ui/WLoading.vue`: Full-screen loading component UI. - `src/components/TheNavigation.vue`: Main navigation component. - `src/components/ScoreForm.vue`: Score editing form. diff --git a/project_task.md b/project_task.md index 537635c..369ae9e 100644 --- a/project_task.md +++ b/project_task.md @@ -25,3 +25,8 @@ - [x] [Task 3] Enhance WMessage Component - [x] Add position support to `src/utils/message.ts` (top/bottom/left/right/center) + +- [x] [Task 4] Implement Fullscreen Loading + - [x] Create `src/components/ui/WLoading.vue` + - [x] Create `src/utils/loading.ts` logic + - [x] Update `src/service/request/index.ts` to handle `showLoading` parameter diff --git a/public/download.png b/public/download.png new file mode 100644 index 0000000..8344fb6 Binary files /dev/null and b/public/download.png differ diff --git a/src/components.d.ts b/src/components.d.ts index e2ea1d0..b8911dd 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -20,6 +20,7 @@ declare module 'vue' { TheFooter: typeof import('./components/TheFooter.vue')['default'] TheInput: typeof import('./components/TheInput.vue')['default'] TheNavigation: typeof import('./components/TheNavigation.vue')['default'] + WLoading: typeof import('./components/ui/WLoading.vue')['default'] WMessage: typeof import('./components/ui/WMessage.vue')['default'] WOption: typeof import('./components/ui/WOption.vue')['default'] WPopconfirm: typeof import('./components/ui/WPopconfirm.vue')['default'] diff --git a/src/components/LoginForm.vue b/src/components/LoginForm.vue index 682b27e..8a60f0c 100644 --- a/src/components/LoginForm.vue +++ b/src/components/LoginForm.vue @@ -1,10 +1,12 @@ + + diff --git a/src/service/api/auth.ts b/src/service/api/auth.ts index 40685d3..553be47 100644 --- a/src/service/api/auth.ts +++ b/src/service/api/auth.ts @@ -12,7 +12,7 @@ export interface LoginResult { } export function login(params: LoginParams) { - return request.post('/user/auth/login', params) + return request.post('/user/auth/login', params, { showLoading: true }) } export function logout() { diff --git a/src/service/api/score.ts b/src/service/api/score.ts index 32b4501..bb035f0 100644 --- a/src/service/api/score.ts +++ b/src/service/api/score.ts @@ -31,9 +31,14 @@ export interface ScoreInfo { } export function saveScore(data: SaveScoreRequest) { - return request.post('/user/score/save-score', data) + return request.post('/user/score/save-score', data, { + showLoading: true + }) } export function getScore() { - return request.get('/user/score', {params: { educationalLevel: '' }, showLoading: false}) + return request.get('/user/score', { + params: { educationalLevel: '' }, + showLoading: false, + }) } diff --git a/src/service/request/index.ts b/src/service/request/index.ts index ae87a4b..ba7a26f 100644 --- a/src/service/request/index.ts +++ b/src/service/request/index.ts @@ -3,6 +3,7 @@ import NProgress from 'nprogress' import CryptoJS from 'crypto-js' import { useUserStore } from '~/stores/user' import message from '~/utils/message' +import loading from '~/utils/loading' import { InternalAxiosRequestConfig } from 'axios' interface CustomRequestConfig extends InternalAxiosRequestConfig { @@ -10,6 +11,11 @@ interface CustomRequestConfig extends InternalAxiosRequestConfig { showError?: boolean } +interface RequestConfig extends AxiosRequestConfig { + showLoading?: boolean, + showError?: boolean +} + interface ApiResponse { code: number message: string @@ -24,37 +30,45 @@ class Request { // Request interceptor this.instance.interceptors.request.use( - (config: InternalAxiosRequestConfig & { showLoading?: boolean }) => { - const customConfig = config as CustomRequestConfig - - if (customConfig.showLoading !== false) { - NProgress.start() - } - const userStore = useUserStore() - customConfig.headers = customConfig.headers || {} - - if (userStore.token) { - customConfig.headers['Authorization'] = 'Bearer ' + userStore.token - } - // Add Signature - const timestamp = Date.now().toString() - const secret = import.meta.env.VITE_API_SECRET || '' - const sign = CryptoJS.MD5(timestamp + secret).toString() - customConfig.headers['X-App-Sign'] = sign - customConfig.headers['X-App-Timestamp'] = timestamp - return customConfig - }, - (error) => { - return Promise.reject(error) - } -) + (config: InternalAxiosRequestConfig & { showLoading?: boolean }) => { + const customConfig = config as CustomRequestConfig + + // NProgress runs by default for visual feedback + NProgress.start() + + // Full screen blocking loading controlled by showLoading parameter + if (customConfig.showLoading) { + loading.show() + } + + const userStore = useUserStore() + customConfig.headers = customConfig.headers || {} + + if (userStore.token) { + customConfig.headers['Authorization'] = 'Bearer ' + userStore.token + } + // Add Signature + const timestamp = Date.now().toString() + const secret = import.meta.env.VITE_API_SECRET || '' + const sign = CryptoJS.MD5(timestamp + secret).toString() + customConfig.headers['X-App-Sign'] = sign + customConfig.headers['X-App-Timestamp'] = timestamp + return customConfig + }, + (error) => { + return Promise.reject(error) + } + ) // Response interceptor this.instance.interceptors.response.use( (response: AxiosResponse) => { const config = response.config as CustomRequestConfig - if (config.showLoading !== false) { - NProgress.done() + + NProgress.done() + + if (config.showLoading) { + loading.hide() } const res = response.data @@ -69,8 +83,11 @@ class Request { }, (error) => { const config = error.config as CustomRequestConfig - if (config?.showLoading !== false) { - NProgress.done() + + NProgress.done() + + if (config?.showLoading) { + loading.hide() } let msg = 'Network Error' @@ -101,43 +118,35 @@ class Request { msg = backendMsg || 'Internal Server Error' break default: - msg = backendMsg || error.message + msg = backendMsg || `Error: ${error.response.status}` } + } else if (error.request) { + msg = 'No response from server' } - // if (config?.showError !== false) { - // message.error(msg) - // } - // 返回包含具体错误信息的 Error 对象 - return Promise.reject(new Error(msg)) + if (config?.showError !== false) { + message.error(msg) + } + + return Promise.reject(error) } ) } - request(config: CustomRequestConfig): Promise { + request(config: RequestConfig): Promise { return this.instance.request(config) } - get(url: string, params?: any, config?: CustomRequestConfig): Promise { - return this.instance.get(url, { ...config, params }) + get(url: string, config?: RequestConfig): Promise { + return this.instance.get(url, config) } - post(url: string, data?: any, config?: CustomRequestConfig): Promise { + post(url: string, data?: any, config?: RequestConfig): Promise { return this.instance.post(url, data, config) } - - put(url: string, data?: any, config?: CustomRequestConfig): Promise { - return this.instance.put(url, data, config) - } - - delete(url: string, config?: CustomRequestConfig): Promise { - return this.instance.delete(url, config) - } } -const request = new Request({ +export default new Request({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000, }) - -export default request diff --git a/src/utils/loading.ts b/src/utils/loading.ts new file mode 100644 index 0000000..ba327b0 --- /dev/null +++ b/src/utils/loading.ts @@ -0,0 +1,37 @@ +import { createVNode, render } from 'vue' +import WLoading from '~/components/ui/WLoading.vue' + +let loadingCount = 0 +let container: HTMLElement | null = null + +const startLoading = () => { + if (loadingCount === 0) { + container = document.createElement('div') + document.body.appendChild(container) + const vnode = createVNode(WLoading) + render(vnode, container) + } + loadingCount++ +} + +const endLoading = () => { + if (loadingCount <= 0) return + loadingCount-- + if (loadingCount === 0 && container) { + // Add a small delay to prevent flickering if another request starts immediately + setTimeout(() => { + if (loadingCount === 0 && container) { + render(null, container) + container.remove() + container = null + } + }, 100) + } +} + +export const loading = { + show: startLoading, + hide: endLoading, +} + +export default loading