feat(web): Q3 前端体验优化 — ErrorBoundary + 5 hooks + 共享类型 + i18n 基础
- ErrorBoundary 组件:全局错误捕获与优雅降级 - 提取 5 个自定义 hooks:useCountUp, useDarkMode, useDebouncedValue, usePaginatedData, useApiRequest - 从 11 个 API 文件提取 PaginatedResponse 共享类型到 api/types.ts - 统一 API 错误处理(api/errors.ts) - client.ts 迁移到 axios adapter 模式(替代废弃的 CancelToken) - 添加 react-i18next 国际化基础设施 + zh-CN 语言包
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import client from './client';
|
||||
import type { PaginatedResponse } from './users';
|
||||
import type { PaginatedResponse } from './types';
|
||||
|
||||
export interface AuditLogItem {
|
||||
id: string;
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 10000,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
// 请求缓存:短时间内相同请求复用结果
|
||||
interface CacheEntry {
|
||||
data: unknown;
|
||||
@@ -19,25 +13,38 @@ function getCacheKey(config: { url?: string; params?: unknown; method?: string }
|
||||
return `${config.method || 'get'}:${config.url || ''}:${JSON.stringify(config.params || {})}`;
|
||||
}
|
||||
|
||||
// Request interceptor: attach access token + cache
|
||||
const defaultAdapter = axios.getAdapter(axios.defaults.adapter);
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 10000,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
adapter: (config) => {
|
||||
// GET 请求检查缓存
|
||||
if (config.method === 'get' && config.url) {
|
||||
const key = getCacheKey(config);
|
||||
const entry = requestCache.get(key);
|
||||
if (entry && Date.now() - entry.timestamp < CACHE_TTL) {
|
||||
return Promise.resolve({
|
||||
data: entry.data,
|
||||
status: 200,
|
||||
statusText: 'OK (cached)',
|
||||
headers: {} as any,
|
||||
config,
|
||||
});
|
||||
}
|
||||
}
|
||||
return defaultAdapter(config);
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor: attach access token
|
||||
client.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// GET 请求检查缓存
|
||||
if (config.method === 'get' && config.url) {
|
||||
const key = getCacheKey(config);
|
||||
const entry = requestCache.get(key);
|
||||
if (entry && Date.now() - entry.timestamp < CACHE_TTL) {
|
||||
const source = axios.CancelToken.source();
|
||||
config.cancelToken = source.token;
|
||||
// 通过适配器返回缓存数据
|
||||
source.cancel(JSON.stringify({ __cached: true, data: entry.data }));
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
@@ -52,14 +59,6 @@ client.interceptors.response.use(
|
||||
return response;
|
||||
},
|
||||
async (error) => {
|
||||
// 处理缓存命中
|
||||
if (axios.isCancel(error)) {
|
||||
const cached = JSON.parse(error.message || '{}');
|
||||
if (cached.__cached) {
|
||||
return { data: cached.data, status: 200, statusText: 'OK (cached)', headers: {}, config: {} };
|
||||
}
|
||||
}
|
||||
|
||||
const originalRequest = error.config;
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
if (isRefreshing) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import client from './client';
|
||||
import type { PaginatedResponse } from './users';
|
||||
import type { PaginatedResponse } from './types';
|
||||
|
||||
export interface DictionaryItemInfo {
|
||||
id: string;
|
||||
|
||||
8
apps/web/src/api/errors.ts
Normal file
8
apps/web/src/api/errors.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function extractErrorMessage(err: unknown, fallback = '操作失败'): string {
|
||||
if (err && typeof err === 'object' && 'response' in err) {
|
||||
const resp = (err as { response?: { data?: { message?: string } } }).response;
|
||||
if (resp?.data?.message) return resp.data.message;
|
||||
}
|
||||
if (err instanceof Error) return err.message;
|
||||
return fallback;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import client from './client';
|
||||
import type { PaginatedResponse } from './users';
|
||||
import type { PaginatedResponse } from './types';
|
||||
|
||||
export interface MessageTemplateInfo {
|
||||
id: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import client from './client';
|
||||
import type { PaginatedResponse } from './users';
|
||||
import type { PaginatedResponse } from './types';
|
||||
|
||||
export interface MessageInfo {
|
||||
id: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import client from './client';
|
||||
import type { PaginatedResponse } from './users';
|
||||
import type { PaginatedResponse } from './types';
|
||||
|
||||
export interface NumberingRuleInfo {
|
||||
id: string;
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import client from './client';
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
import type { PaginatedResponse } from './types';
|
||||
|
||||
export interface PluginEntityInfo {
|
||||
name: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import client from './client';
|
||||
import type { PaginatedResponse } from './users';
|
||||
import type { PaginatedResponse } from './types';
|
||||
|
||||
export interface RoleInfo {
|
||||
id: string;
|
||||
|
||||
7
apps/web/src/api/types.ts
Normal file
7
apps/web/src/api/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
@@ -1,13 +1,6 @@
|
||||
import client from './client';
|
||||
import type { UserInfo } from './auth';
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
import type { PaginatedResponse } from './types';
|
||||
|
||||
export interface CreateUserRequest {
|
||||
username: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import client from './client';
|
||||
import type { PaginatedResponse } from './users';
|
||||
import type { PaginatedResponse } from './types';
|
||||
|
||||
export interface NodeDef {
|
||||
id: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import client from './client';
|
||||
import type { PaginatedResponse } from './users';
|
||||
import type { PaginatedResponse } from './types';
|
||||
|
||||
export interface TokenInfo {
|
||||
id: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import client from './client';
|
||||
import type { PaginatedResponse } from './users';
|
||||
import type { PaginatedResponse } from './types';
|
||||
|
||||
export interface TaskInfo {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user