mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-16 16:07:07 +08:00
fix(web): 统一 API 请求语义并修复鼠标移动发送间隔
- 新增统一 request:同时处理 HTTP 非 2xx 与 success=false,并用 i18n toast 提示错误 - api/index.ts 与 api/config.ts 统一使用同一 request,避免错误处理不一致 - "发送间隔" 仅控制鼠标移动事件频率,WebRTC/WS 行为一致,不影响点击/滚轮
This commit is contained in:
@@ -31,28 +31,7 @@ import type {
|
||||
TtydStatus,
|
||||
} from '@/types/generated'
|
||||
|
||||
const API_BASE = '/api'
|
||||
|
||||
/**
|
||||
* 通用请求函数
|
||||
*/
|
||||
async function request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ message: 'Request failed' }))
|
||||
throw new Error(error.message || `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
import { request } from './request'
|
||||
|
||||
// ===== 全局配置 API =====
|
||||
export const configApi = {
|
||||
|
||||
@@ -1,97 +1,9 @@
|
||||
// API client for One-KVM backend
|
||||
|
||||
import { toast } from 'vue-sonner'
|
||||
import { request, ApiError } from './request'
|
||||
|
||||
const API_BASE = '/api'
|
||||
|
||||
// Toast debounce mechanism - prevent toast spam (5 seconds)
|
||||
const toastDebounceMap = new Map<string, number>()
|
||||
const TOAST_DEBOUNCE_TIME = 5000
|
||||
|
||||
function shouldShowToast(key: string): boolean {
|
||||
const now = Date.now()
|
||||
const lastToastTime = toastDebounceMap.get(key)
|
||||
|
||||
if (!lastToastTime || now - lastToastTime >= TOAST_DEBOUNCE_TIME) {
|
||||
toastDebounceMap.set(key, now)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
class ApiError extends Error {
|
||||
status: number
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = `${API_BASE}${endpoint}`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
// Parse response body (all responses are 200 OK with success field)
|
||||
const data = await response.json().catch(() => ({
|
||||
success: false,
|
||||
message: 'Failed to parse response'
|
||||
}))
|
||||
|
||||
// Check success field - all errors are indicated by success=false
|
||||
if (data && typeof data.success === 'boolean' && !data.success) {
|
||||
const errorMessage = data.message || 'Operation failed'
|
||||
const apiError = new ApiError(response.status, errorMessage)
|
||||
|
||||
console.info(`[API] ${endpoint} failed:`, errorMessage)
|
||||
|
||||
// Show toast notification to user (with debounce)
|
||||
if (shouldShowToast(`error_${endpoint}`)) {
|
||||
toast.error('Operation Failed', {
|
||||
description: errorMessage,
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
|
||||
throw apiError
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
// Network errors or JSON parsing errors
|
||||
if (error instanceof ApiError) {
|
||||
throw error // Already handled above
|
||||
}
|
||||
|
||||
// Network connectivity issues
|
||||
console.info(`[API] Network error for ${endpoint}:`, error)
|
||||
|
||||
// Show toast for network errors (with debounce)
|
||||
if (shouldShowToast('network_error')) {
|
||||
toast.error('Network Error', {
|
||||
description: 'Unable to connect to server. Please check your connection.',
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
|
||||
throw new ApiError(0, 'Network error')
|
||||
}
|
||||
}
|
||||
|
||||
// Auth API
|
||||
export const authApi = {
|
||||
login: (username: string, password: string) =>
|
||||
|
||||
133
web/src/api/request.ts
Normal file
133
web/src/api/request.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { toast } from 'vue-sonner'
|
||||
import i18n from '@/i18n'
|
||||
|
||||
const API_BASE = '/api'
|
||||
|
||||
// Toast debounce mechanism - prevent toast spam (5 seconds)
|
||||
const toastDebounceMap = new Map<string, number>()
|
||||
const TOAST_DEBOUNCE_TIME = 5000
|
||||
|
||||
function shouldShowToast(key: string): boolean {
|
||||
const now = Date.now()
|
||||
const lastToastTime = toastDebounceMap.get(key)
|
||||
|
||||
if (!lastToastTime || now - lastToastTime >= TOAST_DEBOUNCE_TIME) {
|
||||
toastDebounceMap.set(key, now)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function t(key: string, params?: Record<string, unknown>): string {
|
||||
return String(i18n.global.t(key, params as any))
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApiRequestConfig {
|
||||
/**
|
||||
* Enable toast notifications on errors.
|
||||
* Defaults to true to match existing behavior in api/index.ts.
|
||||
*/
|
||||
toastOnError?: boolean
|
||||
/**
|
||||
* Toast debounce key. Defaults to `error_${endpoint}`.
|
||||
*/
|
||||
toastKey?: string
|
||||
}
|
||||
|
||||
function getToastKey(endpoint: string, config?: ApiRequestConfig): string {
|
||||
return config?.toastKey ?? `error_${endpoint}`
|
||||
}
|
||||
|
||||
function getErrorMessage(data: unknown, fallback: string): string {
|
||||
if (data && typeof data === 'object') {
|
||||
const message = (data as any).message
|
||||
if (typeof message === 'string' && message.trim()) return message
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
export async function request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {},
|
||||
config: ApiRequestConfig = {}
|
||||
): Promise<T> {
|
||||
const url = `${API_BASE}${endpoint}`
|
||||
const toastOnError = config.toastOnError !== false
|
||||
const toastKey = getToastKey(endpoint, config)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
const data = await response.json().catch(() => null)
|
||||
|
||||
// Handle HTTP errors (in case backend returns non-2xx)
|
||||
if (!response.ok) {
|
||||
const message = getErrorMessage(data, `HTTP ${response.status}`)
|
||||
if (toastOnError && shouldShowToast(toastKey)) {
|
||||
toast.error(t('api.operationFailed'), {
|
||||
description: message,
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
throw new ApiError(response.status, message)
|
||||
}
|
||||
|
||||
// Handle backend "success=false" convention (even when HTTP is 200)
|
||||
if (data && typeof (data as any).success === 'boolean' && !(data as any).success) {
|
||||
const message = getErrorMessage(data, t('api.operationFailedDesc'))
|
||||
|
||||
if (toastOnError && shouldShowToast(toastKey)) {
|
||||
toast.error(t('api.operationFailed'), {
|
||||
description: message,
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
|
||||
throw new ApiError(response.status, message)
|
||||
}
|
||||
|
||||
// If response body isn't JSON (or empty), treat as failure for callers expecting JSON.
|
||||
if (data === null) {
|
||||
const message = t('api.parseResponseFailed')
|
||||
if (toastOnError && shouldShowToast(toastKey)) {
|
||||
toast.error(t('api.operationFailed'), {
|
||||
description: message,
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
throw new ApiError(response.status, message)
|
||||
}
|
||||
|
||||
return data as T
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) throw error
|
||||
|
||||
if (toastOnError && shouldShowToast('network_error')) {
|
||||
toast.error(t('api.networkError'), {
|
||||
description: t('api.networkErrorDesc'),
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
|
||||
throw new ApiError(0, t('api.networkError'))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user