updates
This commit is contained in:
parent
3b457cf1c2
commit
430c8f314d
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ function handleLoginConfirm(data: LoginResult) {
|
|||
console.warn('接收到登录数据:', data)
|
||||
if(data.user && data.token){
|
||||
closeLoginModal()
|
||||
initScore()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -27,9 +33,14 @@ class Request {
|
|||
(config: InternalAxiosRequestConfig & { showLoading?: boolean }) => {
|
||||
const customConfig = config as CustomRequestConfig
|
||||
|
||||
if (customConfig.showLoading !== false) {
|
||||
// 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 || {}
|
||||
|
||||
|
|
@ -53,8 +64,11 @@ class Request {
|
|||
this.instance.interceptors.response.use(
|
||||
(response: AxiosResponse<ApiResponse>) => {
|
||||
const config = response.config as CustomRequestConfig
|
||||
if (config.showLoading !== false) {
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue