react-typescript-app

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React 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.tsx
src/
├── 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.tsx

TypeScript Configuration

TypeScript 配置

Essential
tsconfig.json
settings:
json
{
  "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.json
核心配置:
json
{
  "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.ts
:
typescript
// 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.ts
中:
typescript
// 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:
    text-slate-400
    or
    text-[#92adc9]
  • Subtle hover:
    hover:border-primary/20
    ,
    hover:bg-white/5
  • Dynamic styles based on state — use inline
    style
    prop:
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-400
    text-[#92adc9]
  • 微妙悬停效果
    hover:border-primary/20
    hover: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

核心测试规则

  1. Import types from centralized location - not local interfaces
  2. Mock data must match full type definitions - include all required properties
  3. Use
    jest.Mocked<typeof module>
    for typed mocks
  1. 从集中位置导入类型 - 不要使用本地接口
  2. 模拟数据必须匹配完整类型定义 - 包含所有必填属性
  3. 使用
    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进行数据可视化,始终将图表包裹在
ResponsiveContainer
中。

Chart 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
radius={[8, 8, 0, 0]}
(rounded top). For horizontal bars use
radius={[0, 8, 8, 0]}
(rounded right).
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().state
:
tsx
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().state
读取筛选条件:
tsx
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.isError
tsx
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
    ['people']
    also invalidates
    ['people', personId]
    (hierarchical)
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.isError

Toast 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>
)}