feat(web): comprehensive frontend performance and UI/UX optimization

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
This commit is contained in:
iven
2026-04-13 01:37:55 +08:00
parent 88f6516fa9
commit e16c1a85d7
34 changed files with 3558 additions and 778 deletions

View File

@@ -6,35 +6,61 @@ const client = axios.create({
headers: { 'Content-Type': 'application/json' },
});
// Request interceptor: attach access token
// 请求缓存:短时间内相同请求复用结果
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;
});
// Response interceptor: auto-refresh on 401
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 = [];
}
// 响应拦截器:缓存 GET 响应 + 自动刷新 token
client.interceptors.response.use(
(response) => response,
(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) => {
const originalRequest = error.config;
// 处理缓存命中
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) => {
@@ -81,4 +107,23 @@ client.interceptors.response.use(
}
);
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;