This commit is contained in:
zhouwentao 2025-12-27 09:39:24 +08:00
parent 3b457cf1c2
commit 430c8f314d
13 changed files with 147 additions and 52 deletions

View File

@ -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 <token>`.
- 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`

View File

@ -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)

View File

@ -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.

View File

@ -25,3 +25,8 @@
- [x] [Task 3] Enhance WMessage Component <!-- id: 21 -->
- [x] Add position support to `src/utils/message.ts` (top/bottom/left/right/center) <!-- id: 22 -->
- [x] [Task 4] Implement Fullscreen Loading <!-- id: 23 -->
- [x] Create `src/components/ui/WLoading.vue` <!-- id: 24 -->
- [x] Create `src/utils/loading.ts` logic <!-- id: 25 -->
- [x] Update `src/service/request/index.ts` to handle `showLoading` parameter <!-- id: 26 -->

BIN
public/download.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

1
src/components.d.ts vendored
View File

@ -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']

View File

@ -1,10 +1,12 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useUserStore } from '~/stores/user'
import { useScoreStore } from '~/stores/score'
import { type LoginParams, type LoginResult } from '~/service/api/auth'
import message from '~/utils/message'
const userStore = useUserStore()
const scoreStore = useScoreStore()
// Emits
const emit = defineEmits<{
@ -30,6 +32,8 @@ async function handleLogin() {
console.warn(loginResult)
if (loginResult.token && loginResult.user) {
message.success('登录成功')
//
// scoreStore.fetchScore()
//
emit('confirm', loginResult)
}

View File

@ -74,6 +74,7 @@ function handleLoginConfirm(data: LoginResult) {
console.warn('接收到登录数据:', data)
if(data.user && data.token){
closeLoginModal()
initScore()
}
}

View File

@ -0,0 +1,16 @@
<script setup lang="ts">
// Simple loading spinner component
</script>
<template>
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/20 backdrop-blur-sm transition-opacity duration-300">
<div class="bg-white/80 dark:bg-gray-800/80 rounded-lg p-4 shadow-xl flex flex-col items-center">
<div class="animate-spin text-blue-600 dark:text-blue-400">
<div class="i-carbon-circle-dash h-10 w-10" />
</div>
<div class="mt-2 text-sm font-medium text-gray-700 dark:text-gray-200">
加载中...
</div>
</div>
</div>
</template>

View File

@ -12,7 +12,7 @@ export interface LoginResult {
}
export function login(params: LoginParams) {
return request.post<LoginResult>('/user/auth/login', params)
return request.post<LoginResult>('/user/auth/login', params, { showLoading: true })
}
export function logout() {

View File

@ -31,9 +31,14 @@ export interface ScoreInfo {
}
export function saveScore(data: SaveScoreRequest) {
return request.post<ScoreInfo>('/user/score/save-score', data)
return request.post<ScoreInfo>('/user/score/save-score', data, {
showLoading: true
})
}
export function getScore() {
return request.get<ScoreInfo>('/user/score', {params: { educationalLevel: '' }, showLoading: false})
return request.get<ScoreInfo>('/user/score', {
params: { educationalLevel: '' },
showLoading: false,
})
}

View File

@ -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<T = any> {
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<ApiResponse>) => {
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<T = any>(config: CustomRequestConfig): Promise<T> {
request<T = any>(config: RequestConfig): Promise<T> {
return this.instance.request(config)
}
get<T = any>(url: string, params?: any, config?: CustomRequestConfig): Promise<T> {
return this.instance.get(url, { ...config, params })
get<T = any>(url: string, config?: RequestConfig): Promise<T> {
return this.instance.get(url, config)
}
post<T = any>(url: string, data?: any, config?: CustomRequestConfig): Promise<T> {
post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
return this.instance.post(url, data, config)
}
put<T = any>(url: string, data?: any, config?: CustomRequestConfig): Promise<T> {
return this.instance.put(url, data, config)
}
delete<T = any>(url: string, config?: CustomRequestConfig): Promise<T> {
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

37
src/utils/loading.ts Normal file
View File

@ -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