react-typescript-app
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact TypeScript Application
React TypeScript 应用
Project Structure
项目结构
src/
├── api/ # API client and endpoint modules
│ ├── client.ts # Base API client with error handling
│ ├── index.ts # Re-exports all API functions
│ └── [feature].ts # Feature-specific endpoints
├── features/ # Feature-based modules (optional)
│ └── [feature]/
│ ├── components/
│ ├── hooks/
│ └── index.ts
├── shared/ # Shared utilities across features
│ ├── components/ # Reusable UI components
│ ├── hooks/ # Custom hooks
│ └── utils/ # Helper functions
├── types/ # Centralized TypeScript definitions
│ └── index.ts # All type exports
├── constants/ # App constants and config
├── App.tsx
└── index.tsxsrc/
├── api/ # API客户端与端点模块
│ ├── client.ts # 带错误处理的基础API客户端
│ ├── index.ts # 重新导出所有API函数
│ └── [feature].ts # 功能专属端点
├── features/ # 基于功能的模块(可选)
│ └── [feature]/
│ ├── components/
│ ├── hooks/
│ └── index.ts
├── shared/ # 跨功能共享工具
│ ├── components/ # 可复用UI组件
│ ├── hooks/ # 自定义Hooks
│ └── utils/ # 辅助函数
├── types/ # 集中式TypeScript类型定义
│ └── index.ts # 所有类型导出
├── constants/ # 应用常量与配置
├── App.tsx
└── index.tsxTypeScript Configuration
TypeScript 配置
Essential settings:
tsconfig.jsonjson
{
"compilerOptions": {
"target": "ES2020",
"lib": ["DOM", "DOM.Iterable", "ES2021"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "src",
"types": ["jest", "node"],
"paths": {
"@/*": ["./*"],
"@/components/*": ["./components/*"],
"@/hooks/*": ["./hooks/*"],
"@/types/*": ["./types/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "build"]
}tsconfig.jsonjson
{
"compilerOptions": {
"target": "ES2020",
"lib": ["DOM", "DOM.Iterable", "ES2021"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "src",
"types": ["jest", "node"],
"paths": {
"@/*": ["./*"],
"@/components/*": ["./components/*"],
"@/hooks/*": ["./hooks/*"],
"@/types/*": ["./types/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "build"]
}Type Definitions Pattern
类型定义模式
Centralize types in :
types/index.tstypescript
// Core data types
export type DataRow = Record<string, unknown>;
// API response types
export interface ApiResponse<T = unknown> {
message?: string;
data?: T;
}
// Component prop types
export interface ModalProps {
open: boolean;
onClose: () => void;
}
// Domain types with all required fields
export interface Transformation {
id: TransformationType;
name: string; // Always include display name
params: TransformationParams;
}将类型集中定义在中:
types/index.tstypescript
// Core data types
export type DataRow = Record<string, unknown>;
// API response types
export interface ApiResponse<T = unknown> {
message?: string;
data?: T;
}
// Component prop types
export interface ModalProps {
open: boolean;
onClose: () => void;
}
// Domain types with all required fields
export interface Transformation {
id: TransformationType;
name: string; // Always include display name
params: TransformationParams;
}API Layer Pattern
API层模式
client.ts - Base client with error handling:
typescript
const API_BASE = import.meta.env.VITE_API_URL || '/api';
export class ApiError extends Error {
constructor(message: string, public status?: number) {
super(message);
this.name = 'ApiError';
}
}
const request = async <T>(path: string, options?: RequestInit): Promise<T> => {
const response = await fetch(`${API_BASE}${path}`, options);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw Object.assign(new ApiError('Request failed'), { status: response.status, error });
}
if (response.status === 204) return null as T;
return response.json();
};
export const apiGet = <T>(path: string): Promise<T> => request(path);
export const apiPost = <T>(path: string, body?: unknown): Promise<T> =>
request(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
});
export const apiPut = <T>(path: string, body?: unknown): Promise<T> =>
request(path, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
});
export const apiDelete = <T>(path: string): Promise<T> => request(path, { method: 'DELETE' });Feature endpoints - Separate files per domain:
typescript
// api/datasets.ts
import { apiGet, apiPost } from './client';
import type { Dataset, Transformation } from '../types';
export const fetchDatasets = (): Promise<Dataset[]> => apiGet('/datasets');
export const saveDataset = (name: string, transformations: Transformation[]) =>
apiPost<{ message: string }>('/datasets/save', { datasetName: name, transformations });client.ts - 带错误处理的基础客户端:
typescript
const API_BASE = import.meta.env.VITE_API_URL || '/api';
export class ApiError extends Error {
constructor(message: string, public status?: number) {
super(message);
this.name = 'ApiError';
}
}
const request = async <T>(path: string, options?: RequestInit): Promise<T> => {
const response = await fetch(`${API_BASE}${path}`, options);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw Object.assign(new ApiError('Request failed'), { status: response.status, error });
}
if (response.status === 204) return null as T;
return response.json();
};
export const apiGet = <T>(path: string): Promise<T> => request(path);
export const apiPost = <T>(path: string, body?: unknown): Promise<T> =>
request(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
});
export const apiPut = <T>(path: string, body?: unknown): Promise<T> =>
request(path, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
});
export const apiDelete = <T>(path: string): Promise<T> => request(path, { method: 'DELETE' });功能端点 - 按领域拆分文件:
typescript
// api/datasets.ts
import { apiGet, apiPost } from './client';
import type { Dataset, Transformation } from '../types';
export const fetchDatasets = (): Promise<Dataset[]> => apiGet('/datasets');
export const saveDataset = (name: string, transformations: Transformation[]) =>
apiPost<{ message: string }>('/datasets/save', { datasetName: name, transformations });Custom Hooks Pattern
自定义Hook模式
typescript
import { useState, useCallback } from 'react';
interface UseErrorReturn {
error: string | null;
showError: (message: string) => void;
clearError: () => void;
}
export function useError(): UseErrorReturn {
const [error, setError] = useState<string | null>(null);
const showError = useCallback((message: string) => {
setError(message);
}, []);
const clearError = useCallback(() => {
setError(null);
}, []);
return { error, showError, clearError };
}typescript
import { useState, useCallback } from 'react';
interface UseErrorReturn {
error: string | null;
showError: (message: string) => void;
clearError: () => void;
}
export function useError(): UseErrorReturn {
const [error, setError] = useState<string | null>(null);
const showError = useCallback((message: string) => {
setError(message);
}, []);
const clearError = useCallback(() => {
setError(null);
}, []);
return { error, showError, clearError };
}ESLint Best Practices
ESLint 最佳实践
Use Optional Chaining
使用可选链操作符
typescript
// ✅ Good
if (!transform?.params) return '';
// ❌ Avoid
if (!transform || !transform.params) return '';typescript
// ✅ Good
if (!transform?.params) return '';
// ❌ Avoid
if (!transform || !transform.params) return '';Use replaceAll() for Global Replacements
使用replaceAll()进行全局替换
typescript
// ✅ Good
const sanitized = name.replaceAll(/[\\/]/g, '_');
// ❌ Avoid
const sanitized = name.replace(/[\\/]/g, '_');typescript
// ✅ Good
const sanitized = name.replaceAll(/[\\/]/g, '_');
// ❌ Avoid
const sanitized = name.replace(/[\\/]/g, '_');Use Object Lookups Instead of Nested Ternaries
使用对象查找替代嵌套三元运算符
typescript
// ✅ Good
const operatorSymbols: Record<string, string> = {
equals: '=',
not_equals: '≠',
greater_than: '>',
less_than: '<',
};
const symbol = operatorSymbols[operator] || '';
// ❌ Avoid
const symbol = operator === 'equals' ? '='
: operator === 'not_equals' ? '≠'
: operator === 'greater_than' ? '>'
: operator === 'less_than' ? '<' : '';typescript
// ✅ Good
const operatorSymbols: Record<string, string> = {
equals: '=',
not_equals: '≠',
greater_than: '>',
less_than: '<',
};
const symbol = operatorSymbols[operator] || '';
// ❌ Avoid
const symbol = operator === 'equals' ? '='
: operator === 'not_equals' ? '≠'
: operator === 'greater_than' ? '>'
: operator === 'less_than' ? '<' : '';Accessibility - Form Elements Need Labels
无障碍设计 - 表单元素需要标签
tsx
// Hidden file inputs still need accessible names
<input
type="file"
className="hidden"
aria-label="Upload CSV, XLSX, or Parquet file"
onChange={handleFileChange}
/>tsx
// Hidden file inputs still need accessible names
<input
type="file"
className="hidden"
aria-label="Upload CSV, XLSX, or Parquet file"
onChange={handleFileChange}
/>Tailwind CSS Styling
Tailwind CSS 样式设计
tailwind.config.js — Dark Theme Tokens
tailwind.config.js — 暗色主题配置
javascript
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
primary: '#2b8cee',
'background-dark': '#101922',
'surface-dark': '#1c2632',
'border-dark': '#233648',
},
fontFamily: {
sans: ['Manrope', 'sans-serif'],
},
},
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
};javascript
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
primary: '#2b8cee',
'background-dark': '#101922',
'surface-dark': '#1c2632',
'border-dark': '#233648',
},
fontFamily: {
sans: ['Manrope', 'sans-serif'],
},
},
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
};Common Patterns
通用样式模式
- Page background:
bg-background-dark - Cards / panels:
bg-surface-dark border border-border-dark rounded-2xl - Primary actions:
bg-primary hover:bg-primary/90 text-white - Muted text: or
text-slate-400text-[#92adc9] - Subtle hover: ,
hover:border-primary/20hover:bg-white/5 - Dynamic styles based on state — use inline prop:
style
tsx
<div style={{
opacity: isLoading ? 0.7 : 1,
pointerEvents: isLoading ? 'none' : 'auto',
}}>- 页面背景:
bg-background-dark - 卡片/面板:
bg-surface-dark border border-border-dark rounded-2xl - 主要操作按钮:
bg-primary hover:bg-primary/90 text-white - 弱化文本:或
text-slate-400text-[#92adc9] - 微妙悬停效果:、
hover:border-primary/20hover:bg-white/5 - 基于状态的动态样式 — 使用内联属性:
style
tsx
<div style={{
opacity: isLoading ? 0 : 1,
pointerEvents: isLoading ? 'none' : 'auto',
}}>Global Styles (index.css)
全局样式(index.css)
css
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
background-color: #101922;
color: white;
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #101922; }
::-webkit-scrollbar-thumb { background: #233648; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #2b8cee; }css
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
background-color: #101922;
color: white;
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #101922; }
::-webkit-scrollbar-thumb { background: #233648; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #2b8cee; }Component Patterns
组件设计模式
Modal Component
模态框组件
tsx
interface ModalProps {
open: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
const Modal: React.FC<ModalProps> = ({ open, onClose, title, children }) => {
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
<div className="relative bg-surface-dark border border-border-dark rounded-2xl p-6 w-full max-w-md shadow-xl">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-white">{title}</h2>
<button onClick={onClose} className="text-slate-400 hover:text-white">
<span className="material-symbols-outlined">close</span>
</button>
</div>
{children}
</div>
</div>
);
};tsx
interface ModalProps {
open: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
const Modal: React.FC<ModalProps> = ({ open, onClose, title, children }) => {
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
<div className="relative bg-surface-dark border border-border-dark rounded-2xl p-6 w-full max-w-md shadow-xl">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-white">{title}</h2>
<button onClick={onClose} className="text-slate-400 hover:text-white">
<span className="material-symbols-outlined">close</span>
</button>
</div>
{children}
</div>
</div>
);
};Save Modal Example
保存模态框示例
tsx
interface SaveModalProps {
open: boolean;
onClose: () => void;
onSave: (name: string) => Promise<void>;
defaultName?: string;
}
const SaveModal: React.FC<SaveModalProps> = ({ open, onClose, onSave, defaultName = '' }) => {
const [name, setName] = useState(defaultName);
const [saving, setSaving] = useState(false);
useEffect(() => {
if (open) setName(defaultName);
}, [open, defaultName]);
const handleSave = async () => {
if (!name.trim()) return;
setSaving(true);
try {
await onSave(name);
onClose();
} finally {
setSaving(false);
}
};
return (
<Modal open={open} onClose={() => !saving && onClose()} title="Save">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter a name..."
className="w-full px-3 py-2 bg-background-dark border border-border-dark rounded-lg text-white text-sm focus:ring-2 focus:ring-primary focus:border-transparent"
/>
<div className="flex justify-end gap-3 mt-4">
<button onClick={onClose} className="px-4 py-2 text-sm text-slate-400 hover:text-white">Cancel</button>
<button onClick={handleSave} disabled={saving} className="px-4 py-2 text-sm bg-primary hover:bg-primary/90 text-white rounded-lg disabled:opacity-50">
{saving ? 'Saving...' : 'Save'}
</button>
</div>
</Modal>
);
};tsx
interface SaveModalProps {
open: boolean;
onClose: () => void;
onSave: (name: string) => Promise<void>;
defaultName?: string;
}
const SaveModal: React.FC<SaveModalProps> = ({ open, onClose, onSave, defaultName = '' }) => {
const [name, setName] = useState(defaultName);
const [saving, setSaving] = useState(false);
useEffect(() => {
if (open) setName(defaultName);
}, [open, defaultName]);
const handleSave = async () => {
if (!name.trim()) return;
setSaving(true);
try {
await onSave(name);
onClose();
} finally {
setSaving(false);
}
};
return (
<Modal open={open} onClose={() => !saving && onClose()} title="Save">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter a name..."
className="w-full px-3 py-2 bg-background-dark border border-border-dark rounded-lg text-white text-sm focus:ring-2 focus:ring-primary focus:border-transparent"
/>
<div className="flex justify-end gap-3 mt-4">
<button onClick={onClose} className="px-4 py-2 text-sm text-slate-400 hover:text-white">Cancel</button>
<button onClick={handleSave} disabled={saving} className="px-4 py-2 text-sm bg-primary hover:bg-primary/90 text-white rounded-lg disabled:opacity-50">
{saving ? 'Saving...' : 'Save'}
</button>
</div>
</Modal>
);
};Testing Patterns
测试模式
Test File Structure
测试文件结构
typescript
import { renderHook, act, waitFor } from '@testing-library/react';
import { useDataProfile } from '../useDataProfile';
import * as api from '../../api';
import type { DataProfile, ColumnStats } from '../../types';
jest.mock('../../api');
const mockedApi = api as jest.Mocked<typeof api>;
describe('useDataProfile', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('should fetch profile data', async () => {
const mockProfile: DataProfile = {
totalRows: 100,
totalColumns: 5,
columns: [
{ name: 'id', declaredType: 'integer', inferredType: 'integer' }
]
};
mockedApi.getProfile.mockResolvedValueOnce(mockProfile);
const { result } = renderHook(() => useDataProfile([], mockShowError));
// ... assertions
});
});typescript
import { renderHook, act, waitFor } from '@testing-library/react';
import { useDataProfile } from '../useDataProfile';
import * as api from '../../api';
import type { DataProfile, ColumnStats } from '../../types';
jest.mock('../../api');
const mockedApi = api as jest.Mocked<typeof api>;
describe('useDataProfile', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('should fetch profile data', async () => {
const mockProfile: DataProfile = {
totalRows: 100,
totalColumns: 5,
columns: [
{ name: 'id', declaredType: 'integer', inferredType: 'integer' }
]
};
mockedApi.getProfile.mockResolvedValueOnce(mockProfile);
const { result } = renderHook(() => useDataProfile([], mockShowError));
// ... assertions
});
});Key Testing Rules
核心测试规则
- Import types from centralized location - not local interfaces
- Mock data must match full type definitions - include all required properties
- Use for typed mocks
jest.Mocked<typeof module>
- 从集中位置导入类型 - 不要使用本地接口
- 模拟数据必须匹配完整类型定义 - 包含所有必填属性
- 使用创建类型化模拟
jest.Mocked<typeof module>
Dependencies
依赖项
json
{
"dependencies": {
"react": "^19.x",
"react-router-dom": "^7.x",
"@tanstack/react-query": "^5.x",
"recharts": "^2.x"
},
"devDependencies": {
"tailwindcss": "^3.x",
"@tailwindcss/forms": "^0.5.x",
"@tailwindcss/typography": "^0.5.x",
"postcss": "^8.x",
"autoprefixer": "^10.x",
"vite": "^6.x",
"typescript": "^5.x",
"@types/react": "^19.x",
"@testing-library/react": "^14.x",
"@testing-library/jest-dom": "^6.x"
}
}json
{
"dependencies": {
"react": "^19.x",
"react-router-dom": "^7.x",
"@tanstack/react-query": "^5.x",
"recharts": "^2.x"
},
"devDependencies": {
"tailwindcss": "^3.x",
"@tailwindcss/forms": "^0.5.x",
"@tailwindcss/typography": "^0.5.x",
"postcss": "^8.x",
"autoprefixer": "^10.x",
"vite": "^6.x",
"typescript": "^5.x",
"@types/react": "^19.x",
"@testing-library/react": "^14.x",
"@testing-library/jest-dom": "^6.x"
}
}Recharts Chart Patterns
Recharts 图表模式
Use Recharts for data visualization. Always wrap charts in .
ResponsiveContainer使用Recharts进行数据可视化,始终将图表包裹在中。
ResponsiveContainerChart Color Palette
图表调色板
Define a shared palette constant for consistent chart colors:
typescript
const CHART_COLORS = ['#0ea5e9', '#8b5cf6', '#10b981', '#f59e0b', '#ef4444', '#ec4899'];定义共享调色板常量以保持图表颜色一致:
typescript
const CHART_COLORS = ['#0ea5e9', '#8b5cf6', '#10b981', '#f59e0b', '#ef4444', '#ec4899'];Dark-Theme Chart Styling
暗色主题图表样式
Apply these consistently to all Recharts components for a polished dark UI:
typescript
// Tooltip — light popup for readability against dark background
const CHART_TOOLTIP_STYLE = {
contentStyle: {
backgroundColor: '#f1f5f9',
border: '1px solid #cbd5e1',
borderRadius: '8px',
color: '#0f172a',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
},
labelStyle: { color: '#0f172a', fontWeight: 'bold' },
itemStyle: { color: '#0f172a' },
};
// Axis — muted ticks that don't compete with data
const CHART_AXIS_STYLE = {
stroke: '#94a3b8',
tick: { fill: '#94a3b8', fontSize: 12 },
tickLine: { stroke: '#475569' },
};
// Grid — subtle dashed lines
const CHART_GRID_STYLE = {
strokeDasharray: '3 3',
stroke: '#334155',
};为所有Recharts组件统一应用以下样式,打造精致的暗色UI:
typescript
// Tooltip — light popup for readability against dark background
const CHART_TOOLTIP_STYLE = {
contentStyle: {
backgroundColor: '#f1f5f9',
border: '1px solid #cbd5e1',
borderRadius: '8px',
color: '#0f172a',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
},
labelStyle: { color: '#0f172a', fontWeight: 'bold' },
itemStyle: { color: '#0f172a' },
};
// Axis — muted ticks that don't compete with data
const CHART_AXIS_STYLE = {
stroke: '#94a3b8',
tick: { fill: '#94a3b8', fontSize: 12 },
tickLine: { stroke: '#475569' },
};
// Grid — subtle dashed lines
const CHART_GRID_STYLE = {
strokeDasharray: '3 3',
stroke: '#334155',
};Donut Chart
环形图
tsx
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
innerRadius={50}
outerRadius={80}
paddingAngle={2}
dataKey="value"
>
{chartData.map((entry, index) => (
<Cell key={entry.name} fill={CHART_COLORS[index % CHART_COLORS.length]} />
))}
</Pie>
<Tooltip {...CHART_TOOLTIP_STYLE} />
</PieChart>
</ResponsiveContainer>tsx
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
innerRadius={50}
outerRadius={80}
paddingAngle={2}
dataKey="value"
>
{chartData.map((entry, index) => (
<Cell key={entry.name} fill={CHART_COLORS[index % CHART_COLORS.length]} />
))}
</Pie>
<Tooltip {...CHART_TOOLTIP_STYLE} />
</PieChart>
</ResponsiveContainer>Line Chart (Trend)
折线图(趋势)
tsx
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
<ResponsiveContainer width="100%" height={300}>
<LineChart data={trendData}>
<CartesianGrid {...CHART_GRID_STYLE} />
<XAxis dataKey="period" {...CHART_AXIS_STYLE} />
<YAxis {...CHART_AXIS_STYLE} />
<Tooltip {...CHART_TOOLTIP_STYLE} />
<Legend />
<Line type="monotone" dataKey="primary" stroke="#0ea5e9" strokeWidth={2} dot={{ r: 4 }} activeDot={{ r: 6 }} />
<Line type="monotone" dataKey="secondary" stroke="#94a3b8" strokeWidth={2} dot={{ r: 4 }} activeDot={{ r: 6 }} />
</LineChart>
</ResponsiveContainer>tsx
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
<ResponsiveContainer width="100%" height={300}>
<LineChart data={trendData}>
<CartesianGrid {...CHART_GRID_STYLE} />
<XAxis dataKey="period" {...CHART_AXIS_STYLE} />
<YAxis {...CHART_AXIS_STYLE} />
<Tooltip {...CHART_TOOLTIP_STYLE} />
<Legend />
<Line type="monotone" dataKey="primary" stroke="#0ea5e9" strokeWidth={2} dot={{ r: 4 }} activeDot={{ r: 6 }} />
<Line type="monotone" dataKey="secondary" stroke="#94a3b8" strokeWidth={2} dot={{ r: 4 }} activeDot={{ r: 6 }} />
</LineChart>
</ResponsiveContainer>Bar Chart
柱状图
tsx
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
<ResponsiveContainer width="100%" height={300}>
<BarChart data={barData} layout="vertical">
<CartesianGrid {...CHART_GRID_STYLE} />
<XAxis type="number" {...CHART_AXIS_STYLE} />
<YAxis type="category" dataKey="name" {...CHART_AXIS_STYLE} width={100} />
<Tooltip {...CHART_TOOLTIP_STYLE} />
<Bar dataKey="value" fill="#8b5cf6" radius={[0, 8, 8, 0]} />
</BarChart>
</ResponsiveContainer>For vertical bars use (rounded top). For horizontal bars use (rounded right).
radius={[8, 8, 0, 0]}radius={[0, 8, 8, 0]}tsx
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
<ResponsiveContainer width="100%" height={300}>
<BarChart data={barData} layout="vertical">
<CartesianGrid {...CHART_GRID_STYLE} />
<XAxis type="number" {...CHART_AXIS_STYLE} />
<YAxis type="category" dataKey="name" {...CHART_AXIS_STYLE} width={100} />
<Tooltip {...CHART_TOOLTIP_STYLE} />
<Bar dataKey="value" fill="#8b5cf6" radius={[0, 8, 8, 0]} />
</BarChart>
</ResponsiveContainer>垂直柱状图使用(顶部圆角)。水平柱状图使用(右侧圆角)。
radius={[8, 8, 0, 0]}radius={[0, 8, 8, 0]}Interactive Chart Drill-Down
交互式图表钻取
Charts can navigate to detail pages with pre-applied filters via React Router state:
tsx
import { useNavigate } from 'react-router-dom';
const navigate = useNavigate();
<Pie
data={roleData}
dataKey="value"
onClick={(data) => navigate('/people', { state: { role: data.name } })}
cursor="pointer"
/>The target page reads the filter from :
useLocation().statetsx
const location = useLocation();
const initialFilter = location.state?.role || '';图表可通过React Router状态导航至详情页,并自动应用预设置的筛选条件:
tsx
import { useNavigate } from 'react-router-dom';
const navigate = useNavigate();
<Pie
data={roleData}
dataKey="value"
onClick={(data) => navigate('/people', { state: { role: data.name } })}
cursor="pointer"
/>目标页面通过读取筛选条件:
useLocation().statetsx
const location = useLocation();
const initialFilter = location.state?.role || '';Dashboard Cards
仪表盘卡片
MetricCard Component
MetricCard 组件
A reusable card for displaying key metrics in a dashboard grid:
tsx
interface MetricCardProps {
label: string;
value: number | string;
subtitle: string;
color: string; // Tailwind text color class, e.g. 'text-sky-400'
icon: string; // Material Symbols icon name
loading?: boolean;
}
const MetricCard: React.FC<MetricCardProps> = ({ label, value, subtitle, color, icon, loading }) => (
<div className="bg-surface-dark border border-border-dark p-6 rounded-2xl shadow-sm hover:border-primary/20 transition-all group">
<div className="flex justify-between items-start mb-4">
<div className={`p-2 rounded-lg bg-white/5 ${color} group-hover:scale-110 transition-transform`}>
<span className="material-symbols-outlined">{icon}</span>
</div>
</div>
<p className="text-slate-500 text-xs font-bold uppercase tracking-widest mb-1">{label}</p>
<h2 className={`text-4xl font-black ${color}`}>{loading ? '—' : value}</h2>
<p className="text-[#92adc9] text-xs mt-2 opacity-60 italic">{subtitle}</p>
</div>
);Usage in a dashboard grid:
tsx
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<MetricCard label="Total Users" value={1234} subtitle="Active this month" color="text-sky-400" icon="group" />
<MetricCard label="Revenue" value="$45K" subtitle="vs $38K last month" color="text-emerald-400" icon="payments" />
<MetricCard label="Errors" value={12} subtitle="3 critical" color="text-red-400" icon="error" />
</div>用于在仪表板网格中展示关键指标的可复用卡片:
tsx
interface MetricCardProps {
label: string;
value: number | string;
subtitle: string;
color: string; // Tailwind text color class, e.g. 'text-sky-400'
icon: string; // Material Symbols icon name
loading?: boolean;
}
const MetricCard: React.FC<MetricCardProps> = ({ label, value, subtitle, color, icon, loading }) => (
<div className="bg-surface-dark border border-border-dark p-6 rounded-2xl shadow-sm hover:border-primary/20 transition-all group">
<div className="flex justify-between items-start mb-4">
<div className={`p-2 rounded-lg bg-white/5 ${color} group-hover:scale-110 transition-transform`}>
<span className="material-symbols-outlined">{icon}</span>
</div>
</div>
<p className="text-slate-500 text-xs font-bold uppercase tracking-widest mb-1">{label}</p>
<h2 className={`text-4xl font-black ${color}`}>{loading ? '—' : value}</h2>
<p className="text-[#92adc9] text-xs mt-2 opacity-60 italic">{subtitle}</p>
</div>
);React Query (TanStack Query)
在仪表板网格中的使用示例:
Use @tanstack/react-query for server state management. It handles caching, refetching, loading/error states, and cache invalidation.
tsx
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<MetricCard label="Total Users" value={1234} subtitle="Active this month" color="text-sky-400" icon="group" />
<MetricCard label="Revenue" value="$45K" subtitle="vs $38K last month" color="text-emerald-400" icon="payments" />
<MetricCard label="Errors" value={12} subtitle="3 critical" color="text-red-400" icon="error" />
</div>Setup
React Query(TanStack Query)
tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
);使用**@tanstack/react-query**管理服务端状态,它可处理缓存、重新获取、加载/错误状态及缓存失效。
Queries (reading data)
配置
tsx
import { useQuery } from '@tanstack/react-query';
import { fetchPeople } from '../api/people';
const { data: people, isLoading, error } = useQuery({
queryKey: ['people'],
queryFn: () => fetchPeople(''),
});tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
);Mutations (creating/updating/deleting)
查询(读取数据)
tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createPerson } from '../api/people';
const queryClient = useQueryClient();
const createMutation = useMutation({
mutationFn: createPerson,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['people'] });
handleCloseModal();
},
});
// Trigger: createMutation.mutate({ name: 'Alice', role: 'Engineer' });
// Status: createMutation.isPending, createMutation.isErrortsx
import { useQuery } from '@tanstack/react-query';
import { fetchPeople } from '../api/people';
const { data: people, isLoading, error } = useQuery({
queryKey: ['people'],
queryFn: () => fetchPeople(''),
});Query Key Conventions
变更(创建/更新/删除)
- Use arrays: ,
['people'],['people', personId]['people', { role: 'engineer' }] - Invalidating also invalidates
['people'](hierarchical)['people', personId]
tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createPerson } from '../api/people';
const queryClient = useQueryClient();
const createMutation = useMutation({
mutationFn: createPerson,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['people'] });
handleCloseModal();
},
});
// Trigger: createMutation.mutate({ name: 'Alice', role: 'Engineer' });
// Status: createMutation.isPending, createMutation.isErrorToast Notifications
查询键约定
Lightweight toast pattern using CSS animations — no library needed.
- 使用数组格式:、
['people']、['people', personId]['people', { role: 'engineer' }] - 失效时,也会失效
['people'](层级关系)['people', personId]
CSS (add to index.css)
提示通知(Toast)
css
@keyframes slide-in {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
.animate-slide-in { animation: slide-in 0.3s ease-out; }
.animate-fade-out { animation: fade-out 0.3s ease-out forwards; }使用CSS动画实现轻量级提示通知,无需依赖额外库。
Hook
CSS(添加至index.css)
tsx
function useToast(duration = 3000) {
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
const [fading, setFading] = useState(false);
const showToast = useCallback((message: string, type: 'success' | 'error' = 'success') => {
setToast({ message, type });
setFading(false);
setTimeout(() => setFading(true), duration - 300);
setTimeout(() => setToast(null), duration);
}, [duration]);
return { toast, fading, showToast };
}css
@keyframes slide-in {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
.animate-slide-in { animation: slide-in 0.3s ease-out; }
.animate-fade-out { animation: fade-out 0.3s ease-out forwards; }Render
自定义Hook
tsx
{toast && (
<div className={`fixed bottom-6 right-6 z-50 px-4 py-3 rounded-lg shadow-lg text-white text-sm
${toast.type === 'success' ? 'bg-emerald-600' : 'bg-red-600'}
${fading ? 'animate-fade-out' : 'animate-slide-in'}`}
>
{toast.message}
</div>
)}tsx
function useToast(duration = 3000) {
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
const [fading, setFading] = useState(false);
const showToast = useCallback((message: string, type: 'success' | 'error' = 'success') => {
setToast({ message, type });
setFading(false);
setTimeout(() => setFading(true), duration - 300);
setTimeout(() => setToast(null), duration);
}, [duration]);
return { toast, fading, showToast };
}—
渲染
—
tsx
{toast && (
<div className={`fixed bottom-6 right-6 z-50 px-4 py-3 rounded-lg shadow-lg text-white text-sm
${toast.type === 'success' ? 'bg-emerald-600' : 'bg-red-600'}
${fading ? 'animate-fade-out' : 'animate-slide-in'}`}
>
{toast.message}
</div>
)}