updates
This commit is contained in:
parent
3b457cf1c2
commit
430c8f314d
|
|
@ -8,6 +8,11 @@
|
||||||
- **Implementation**: Programmatically mounts `WMessage.vue`.
|
- **Implementation**: Programmatically mounts `WMessage.vue`.
|
||||||
- **New Feature**: Supports `position` argument: `'top-center' | 'top-left' | 'top-right' | 'bottom-center' | 'bottom-left' | 'bottom-right'`.
|
- **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`
|
### `src/service/request/index.ts`
|
||||||
- **Purpose**: Encapsulates Axios for API requests.
|
- **Purpose**: Encapsulates Axios for API requests.
|
||||||
- **Features**:
|
- **Features**:
|
||||||
|
|
@ -15,7 +20,9 @@
|
||||||
- Adds `Authorization: Bearer <token>`.
|
- Adds `Authorization: Bearer <token>`.
|
||||||
- Adds `X-App-Sign` and `X-App-Timestamp` headers.
|
- Adds `X-App-Sign` and `X-App-Timestamp` headers.
|
||||||
- Starts `NProgress`.
|
- 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`.
|
- 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`.
|
- Config: `showLoading`, `showError`.
|
||||||
|
|
||||||
### `src/service/api/auth.ts`
|
### `src/service/api/auth.ts`
|
||||||
|
|
|
||||||
|
|
@ -41,3 +41,11 @@
|
||||||
- **Scope**:
|
- **Scope**:
|
||||||
- `src/utils/message.ts` (Update logic to support multiple containers)
|
- `src/utils/message.ts` (Update logic to support multiple containers)
|
||||||
- `src/components/ui/WMessage.vue` (No changes needed, styles handled by container)
|
- `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
|
# Project File Index
|
||||||
|
|
||||||
- `src/utils/message.ts`: Global message utility.
|
- `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/request/index.ts`: Axios wrapper.
|
||||||
- `src/service/api/auth.ts`: Authentication API.
|
- `src/service/api/auth.ts`: Authentication API.
|
||||||
- `src/service/api/score.ts`: Score API.
|
- `src/service/api/score.ts`: Score API.
|
||||||
- `src/stores/user.ts`: User Pinia store.
|
- `src/stores/user.ts`: User Pinia store.
|
||||||
- `src/stores/score.ts`: Score Pinia store.
|
- `src/stores/score.ts`: Score Pinia store.
|
||||||
- `src/components/ui/WMessage.vue`: Message component UI.
|
- `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/TheNavigation.vue`: Main navigation component.
|
||||||
- `src/components/ScoreForm.vue`: Score editing form.
|
- `src/components/ScoreForm.vue`: Score editing form.
|
||||||
|
|
|
||||||
|
|
@ -25,3 +25,8 @@
|
||||||
|
|
||||||
- [x] [Task 3] Enhance WMessage Component <!-- id: 21 -->
|
- [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] 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']
|
TheFooter: typeof import('./components/TheFooter.vue')['default']
|
||||||
TheInput: typeof import('./components/TheInput.vue')['default']
|
TheInput: typeof import('./components/TheInput.vue')['default']
|
||||||
TheNavigation: typeof import('./components/TheNavigation.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']
|
WMessage: typeof import('./components/ui/WMessage.vue')['default']
|
||||||
WOption: typeof import('./components/ui/WOption.vue')['default']
|
WOption: typeof import('./components/ui/WOption.vue')['default']
|
||||||
WPopconfirm: typeof import('./components/ui/WPopconfirm.vue')['default']
|
WPopconfirm: typeof import('./components/ui/WPopconfirm.vue')['default']
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useUserStore } from '~/stores/user'
|
import { useUserStore } from '~/stores/user'
|
||||||
|
import { useScoreStore } from '~/stores/score'
|
||||||
import { type LoginParams, type LoginResult } from '~/service/api/auth'
|
import { type LoginParams, type LoginResult } from '~/service/api/auth'
|
||||||
import message from '~/utils/message'
|
import message from '~/utils/message'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const scoreStore = useScoreStore()
|
||||||
|
|
||||||
// 定义 Emits
|
// 定义 Emits
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
@ -30,6 +32,8 @@ async function handleLogin() {
|
||||||
console.warn(loginResult)
|
console.warn(loginResult)
|
||||||
if (loginResult.token && loginResult.user) {
|
if (loginResult.token && loginResult.user) {
|
||||||
message.success('登录成功')
|
message.success('登录成功')
|
||||||
|
// 获取分数信息
|
||||||
|
// scoreStore.fetchScore()
|
||||||
// 抛出事件
|
// 抛出事件
|
||||||
emit('confirm', loginResult)
|
emit('confirm', loginResult)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ function handleLoginConfirm(data: LoginResult) {
|
||||||
console.warn('接收到登录数据:', data)
|
console.warn('接收到登录数据:', data)
|
||||||
if(data.user && data.token){
|
if(data.user && data.token){
|
||||||
closeLoginModal()
|
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) {
|
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() {
|
export function logout() {
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,14 @@ export interface ScoreInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveScore(data: SaveScoreRequest) {
|
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() {
|
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 CryptoJS from 'crypto-js'
|
||||||
import { useUserStore } from '~/stores/user'
|
import { useUserStore } from '~/stores/user'
|
||||||
import message from '~/utils/message'
|
import message from '~/utils/message'
|
||||||
|
import loading from '~/utils/loading'
|
||||||
import { InternalAxiosRequestConfig } from 'axios'
|
import { InternalAxiosRequestConfig } from 'axios'
|
||||||
|
|
||||||
interface CustomRequestConfig extends InternalAxiosRequestConfig {
|
interface CustomRequestConfig extends InternalAxiosRequestConfig {
|
||||||
|
|
@ -10,6 +11,11 @@ interface CustomRequestConfig extends InternalAxiosRequestConfig {
|
||||||
showError?: boolean
|
showError?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RequestConfig extends AxiosRequestConfig {
|
||||||
|
showLoading?: boolean,
|
||||||
|
showError?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
interface ApiResponse<T = any> {
|
interface ApiResponse<T = any> {
|
||||||
code: number
|
code: number
|
||||||
message: string
|
message: string
|
||||||
|
|
@ -24,37 +30,45 @@ class Request {
|
||||||
|
|
||||||
// Request interceptor
|
// Request interceptor
|
||||||
this.instance.interceptors.request.use(
|
this.instance.interceptors.request.use(
|
||||||
(config: InternalAxiosRequestConfig & { showLoading?: boolean }) => {
|
(config: InternalAxiosRequestConfig & { showLoading?: boolean }) => {
|
||||||
const customConfig = config as CustomRequestConfig
|
const customConfig = config as CustomRequestConfig
|
||||||
|
|
||||||
if (customConfig.showLoading !== false) {
|
// NProgress runs by default for visual feedback
|
||||||
NProgress.start()
|
NProgress.start()
|
||||||
}
|
|
||||||
const userStore = useUserStore()
|
|
||||||
customConfig.headers = customConfig.headers || {}
|
|
||||||
|
|
||||||
if (userStore.token) {
|
// Full screen blocking loading controlled by showLoading parameter
|
||||||
customConfig.headers['Authorization'] = 'Bearer ' + userStore.token
|
if (customConfig.showLoading) {
|
||||||
}
|
loading.show()
|
||||||
// Add Signature
|
}
|
||||||
const timestamp = Date.now().toString()
|
|
||||||
const secret = import.meta.env.VITE_API_SECRET || ''
|
const userStore = useUserStore()
|
||||||
const sign = CryptoJS.MD5(timestamp + secret).toString()
|
customConfig.headers = customConfig.headers || {}
|
||||||
customConfig.headers['X-App-Sign'] = sign
|
|
||||||
customConfig.headers['X-App-Timestamp'] = timestamp
|
if (userStore.token) {
|
||||||
return customConfig
|
customConfig.headers['Authorization'] = 'Bearer ' + userStore.token
|
||||||
},
|
}
|
||||||
(error) => {
|
// Add Signature
|
||||||
return Promise.reject(error)
|
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
|
// Response interceptor
|
||||||
this.instance.interceptors.response.use(
|
this.instance.interceptors.response.use(
|
||||||
(response: AxiosResponse<ApiResponse>) => {
|
(response: AxiosResponse<ApiResponse>) => {
|
||||||
const config = response.config as CustomRequestConfig
|
const config = response.config as CustomRequestConfig
|
||||||
if (config.showLoading !== false) {
|
|
||||||
NProgress.done()
|
NProgress.done()
|
||||||
|
|
||||||
|
if (config.showLoading) {
|
||||||
|
loading.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = response.data
|
const res = response.data
|
||||||
|
|
@ -69,8 +83,11 @@ class Request {
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
const config = error.config as CustomRequestConfig
|
const config = error.config as CustomRequestConfig
|
||||||
if (config?.showLoading !== false) {
|
|
||||||
NProgress.done()
|
NProgress.done()
|
||||||
|
|
||||||
|
if (config?.showLoading) {
|
||||||
|
loading.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = 'Network Error'
|
let msg = 'Network Error'
|
||||||
|
|
@ -101,43 +118,35 @@ class Request {
|
||||||
msg = backendMsg || 'Internal Server Error'
|
msg = backendMsg || 'Internal Server Error'
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
msg = backendMsg || error.message
|
msg = backendMsg || `Error: ${error.response.status}`
|
||||||
}
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
msg = 'No response from server'
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (config?.showError !== false) {
|
if (config?.showError !== false) {
|
||||||
// message.error(msg)
|
message.error(msg)
|
||||||
// }
|
}
|
||||||
// 返回包含具体错误信息的 Error 对象
|
|
||||||
return Promise.reject(new 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)
|
return this.instance.request(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
get<T = any>(url: string, params?: any, config?: CustomRequestConfig): Promise<T> {
|
get<T = any>(url: string, config?: RequestConfig): Promise<T> {
|
||||||
return this.instance.get(url, { ...config, params })
|
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)
|
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,
|
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||||
timeout: 10000,
|
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