Performance improvements: - Vite build: manual chunks, terser minification, optimizeDeps - API response caching with 5s TTL via axios interceptors - React.memo for SidebarMenuItem, useCallback for handlers - CSS classes replacing inline styles to reduce reflows UI/UX enhancements (inspired by SAP Fiori, Linear, Feishu): - Dashboard: trend indicators, sparkline charts, CountUp animation on stat cards - Dashboard: pending tasks section with priority labels - Dashboard: recent activity timeline - Design system tokens: trend colors, line-height, dark mode refinements - Enhanced quick actions with hover animations Accessibility (Lighthouse 100/100): - Skip-to-content link, ARIA landmarks, heading hierarchy - prefers-reduced-motion support, focus-visible states - Color contrast fixes: all text meets 4.5:1 ratio - Keyboard navigation for stat cards and task items SEO: meta theme-color, format-detection, robots.txt
130 lines
3.7 KiB
TypeScript
130 lines
3.7 KiB
TypeScript
import axios from 'axios';
|
|
|
|
const client = axios.create({
|
|
baseURL: '/api/v1',
|
|
timeout: 10000,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
|
|
// 请求缓存:短时间内相同请求复用结果
|
|
interface CacheEntry {
|
|
data: unknown;
|
|
timestamp: number;
|
|
}
|
|
|
|
const requestCache = new Map<string, CacheEntry>();
|
|
const CACHE_TTL = 5000; // 5 秒缓存
|
|
|
|
function getCacheKey(config: { url?: string; params?: unknown; method?: string }): string {
|
|
return `${config.method || 'get'}:${config.url || ''}:${JSON.stringify(config.params || {})}`;
|
|
}
|
|
|
|
// Request interceptor: attach access token + cache
|
|
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;
|
|
});
|
|
|
|
// 响应拦截器:缓存 GET 响应 + 自动刷新 token
|
|
client.interceptors.response.use(
|
|
(response) => {
|
|
// 缓存 GET 响应
|
|
if (response.config.method === 'get' && response.config.url) {
|
|
const key = getCacheKey(response.config);
|
|
requestCache.set(key, { data: response.data, timestamp: Date.now() });
|
|
}
|
|
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) {
|
|
return new Promise((resolve, reject) => {
|
|
failedQueue.push({ resolve, reject });
|
|
}).then((token) => {
|
|
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
return client(originalRequest);
|
|
});
|
|
}
|
|
|
|
originalRequest._retry = true;
|
|
isRefreshing = true;
|
|
|
|
try {
|
|
const refreshToken = localStorage.getItem('refresh_token');
|
|
if (!refreshToken) throw new Error('No refresh token');
|
|
|
|
const { data } = await axios.post('/api/v1/auth/refresh', {
|
|
refresh_token: refreshToken,
|
|
});
|
|
|
|
const newAccessToken = data.data.access_token;
|
|
const newRefreshToken = data.data.refresh_token;
|
|
|
|
localStorage.setItem('access_token', newAccessToken);
|
|
localStorage.setItem('refresh_token', newRefreshToken);
|
|
|
|
processQueue(null, newAccessToken);
|
|
|
|
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
|
|
return client(originalRequest);
|
|
} catch (refreshError) {
|
|
processQueue(refreshError, null);
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('refresh_token');
|
|
window.location.hash = '#/login';
|
|
return Promise.reject(refreshError);
|
|
} finally {
|
|
isRefreshing = false;
|
|
}
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
let isRefreshing = false;
|
|
let failedQueue: Array<{
|
|
resolve: (token: string) => void;
|
|
reject: (error: unknown) => void;
|
|
}> = [];
|
|
|
|
function processQueue(error: unknown, token: string | null) {
|
|
failedQueue.forEach(({ resolve, reject }) => {
|
|
if (token) resolve(token);
|
|
else reject(error);
|
|
});
|
|
failedQueue = [];
|
|
}
|
|
|
|
// 清除缓存(登录/登出时调用)
|
|
export function clearApiCache() {
|
|
requestCache.clear();
|
|
}
|
|
|
|
export default client;
|