🚀 Claude Code 进阶完全指南(六):项目实战案例

0 阅读23分钟

写在前面

前五篇我们学习了 Claude Code 的基础知识、提示词技巧、代码生成、文件操作和 MCP/Skills。今天终于到了实战环节!我们将用三个完整的项目案例,展示如何从零开始使用 Claude Code 开发真实项目。

特别说明:本文提供的是可以直接复制使用的完整代码,每个案例都经过验证,确保可以实际运行。


🎯 一、实战概览

本期案例

案例难度技术栈时长学习重点
📝 Todo 待办应用React + localStorage15 分钟基础交互、状态管理
🌤️ 天气查询应用⭐⭐React + API25 分钟API 调用、异步处理
📊 数据仪表盘⭐⭐⭐React + Recharts45 分钟图表可视化、复杂状态

实战学习路径

第1步:创建项目骨架(项目初始化)
    ↓
第2步:实现核心功能(从简单到复杂)
    ↓
第3步:完善交互细节(加载、错误、空状态)
    ↓
第4步:代码审查与优化(使用 Skills)
    ↓
第5步:测试与验证(确保功能正确)

📝 案例一:Todo 待办应用(15 分钟)

学习目标

  • 掌握 React 基础组件结构
  • 理解状态管理与数据持久化
  • 学习自定义 Hook 的创建

步骤 1:初始化项目

首先,让我们创建项目。在终端中运行:

# 进入工作目录
cd /your/workspace

# 创建 Vite + React 项目
npm create vite@latest todo-app -- --template react

# 进入项目目录
cd todo-app

# 安装依赖
npm install

# 启动开发服务器(保持运行)
npm run dev

提示

保持 npm run dev 在后台运行,这样你可以实时看到代码变化的效果。

步骤 2:启动 Claude Code 并开发

在新的终端窗口中,启动 Claude Code:

claude

然后输入以下指令:

帮我创建一个完整的 Todo 待办应用,包含以下功能:
1. 添加待办事项(输入内容后按回车或点击按钮添加)
2. 显示待办列表(显示所有待办,支持滚动)
3. 标记完成(点击事项切换完成状态,已完成显示删除线)
4. 删除待办(点击删除按钮移除待办)
5. 数据持久化(使用 localStorage,刷新页面不丢失)
6. 统计显示(显示待办总数和已完成数量)
7. 清空已完成(一键删除所有已完成的项目)
8. 响应式设计(手机和电脑都能正常显示)

样式要求:
- 简洁美观的主题
- 完成状态有明显视觉区分
- 友好的空状态提示

请创建完整的文件,确保可以直接运行。

步骤 3:查看生成的文件

Claude Code 会为你创建以下文件结构:

src/
├── App.jsx              # 主应用组件
├── main.jsx             # React 入口
├── components/
│   ├── TodoInput.jsx    # 输入组件
│   ├── TodoItem.jsx    # 待办项组件
│   └── TodoStats.jsx   # 统计组件
├── hooks/
│   └── useTodos.js     # 待办事项 Hook(核心逻辑)
├── App.css             # 主样式文件
└── index.css           # 全局样式

步骤 4:核心代码展示

以下是 Claude Code 生成的核心代码,你可以直接参考:

useTodos.js(核心 Hook)

import { useState, useEffect } from 'react';

export function useTodos() {
  // 从 localStorage 加载初始数据
  const [todos, setTodos] = useState(() => {
    const saved = localStorage.getItem('todos');
    return saved ? JSON.parse(saved) : [];
  });

  // 数据变化时保存到 localStorage
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  // 添加待办
  const addTodo = (text) => {
    if (!text.trim()) return;
    setTodos([
      ...todos,
      {
        id: Date.now(),
        text: text.trim(),
        completed: false,
        createdAt: new Date().toISOString()
      }
    ]);
  };

  // 切换完成状态
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  // 删除待办
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  // 清空已完成
  const clearCompleted = () => {
    setTodos(todos.filter(todo => !todo.completed));
  };

  // 统计信息
  const stats = {
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
    active: todos.filter(t => !t.completed).length
  };

  return { todos, addTodo, toggleTodo, deleteTodo, clearCompleted, stats };
}

App.jsx(主组件)

import { useTodos } from './hooks/useTodos';
import TodoInput from './components/TodoInput';
import TodoItem from './components/TodoItem';
import TodoStats from './components/TodoStats';
import './App.css';

function App() {
  const { todos, addTodo, toggleTodo, deleteTodo, clearCompleted, stats } = useTodos();

  return (
    <div className="app">
      <div className="container">
        <header className="header">
          <h1>📝 待办事项</h1>
          <TodoStats stats={stats} onClearCompleted={clearCompleted} />
        </header>

        <TodoInput onAdd={addTodo} />

        <ul className="todo-list">
          {todos.length === 0 ? (
            <li className="empty-state">
              <span className="empty-icon"></span>
              <p>暂无待办事项</p>
              <p className="empty-hint">在上方添加你的第一个待办吧!</p>
            </li>
          ) : (
            todos.map(todo => (
              <TodoItem
                key={todo.id}
                todo={todo}
                onToggle={toggleTodo}
                onDelete={deleteTodo}
              />
            ))
          )}
        </ul>
      </div>
    </div>
  );
}

export default App;

TodoInput.jsx(输入组件)

import { useState } from 'react';

export default function TodoInput({ onAdd }) {
  const [text, setText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      onAdd(text);
      setText('');
    }
  };

  return (
    <form className="todo-input" onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="添加新的待办事项..."
        className="input"
      />
      <button type="submit" className="add-btn">
        添加
      </button>
    </form>
  );
}

TodoItem.jsx(待办项组件)

export default function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
      <label className="checkbox-label">
        <input
          type="checkbox"
          checked={todo.completed}
          onChange={() => onToggle(todo.id)}
        />
        <span className="custom-checkbox"></span>
        <span className="todo-text">{todo.text}</span>
      </label>
      <button
        className="delete-btn"
        onClick={() => onDelete(todo.id)}
        aria-label="删除"
      >
        ×
      </button>
    </li>
  );
}

App.css(完整样式)

/* 全局重置 */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  padding: 20px;
}

.app {
  max-width: 500px;
  margin: 0 auto;
}

.container {
  background: white;
  border-radius: 16px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
  padding: 24px;
}

.header {
  text-align: center;
  margin-bottom: 24px;
}

.header h1 {
  color: #333;
  font-size: 28px;
  margin-bottom: 16px;
}

/* 输入区域 */
.todo-input {
  display: flex;
  gap: 12px;
  margin-bottom: 20px;
}

.input {
  flex: 1;
  padding: 14px 16px;
  border: 2px solid #e0e0e0;
  border-radius: 12px;
  font-size: 16px;
  transition: border-color 0.2s;
}

.input:focus {
  outline: none;
  border-color: #667eea;
}

.add-btn {
  padding: 14px 24px;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 12px;
  font-size: 16px;
  cursor: pointer;
  transition: background 0.2s;
}

.add-btn:hover {
  background: #5a6fd6;
}

/* 待办列表 */
.todo-list {
  list-style: none;
}

.todo-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px;
  border-bottom: 1px solid #f0f0f0;
  transition: background 0.2s;
}

.todo-item:hover {
  background: #fafafa;
}

.todo-item.completed .todo-text {
  text-decoration: line-through;
  color: #999;
}

.checkbox-label {
  display: flex;
  align-items: center;
  gap: 12px;
  cursor: pointer;
  flex: 1;
}

.checkbox-label input {
  display: none;
}

.custom-checkbox {
  width: 22px;
  height: 22px;
  border: 2px solid #ddd;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.2s;
}

.checkbox-label input:checked + .custom-checkbox {
  background: #667eea;
  border-color: #667eea;
}

.checkbox-label input:checked + .custom-checkbox::after {
  content: '✓';
  color: white;
  font-size: 14px;
}

.todo-text {
  font-size: 16px;
  color: #333;
}

.delete-btn {
  width: 28px;
  height: 28px;
  border: none;
  background: #ff4d4f;
  color: white;
  border-radius: 8px;
  font-size: 18px;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.2s;
}

.todo-item:hover .delete-btn {
  opacity: 1;
}

/* 空状态 */
.empty-state {
  text-align: center;
  padding: 40px 20px;
  color: #999;
}

.empty-icon {
  font-size: 48px;
  display: block;
  margin-bottom: 12px;
}

.empty-hint {
  font-size: 14px;
  margin-top: 8px;
}

/* 统计区域 */
.stats {
  display: flex;
  justify-content: center;
  gap: 20px;
  margin-top: 12px;
}

.stat-item {
  text-align: center;
}

.stat-value {
  font-size: 24px;
  font-weight: bold;
  color: #667eea;
}

.stat-label {
  font-size: 12px;
  color: #999;
}

.clear-btn {
  margin-top: 16px;
  padding: 8px 16px;
  background: transparent;
  border: 1px solid #ff4d4f;
  color: #ff4d4f;
  border-radius: 8px;
  cursor: pointer;
  font-size: 14px;
}

.clear-btn:hover {
  background: #ff4d4f;
  color: white;
}

步骤 5:验证运行

  1. 打开浏览器访问 http://localhost:5173
  2. 尝试添加几个待办事项
  3. 标记几个为完成
  4. 删除一些
  5. 刷新页面,确认数据仍然保留

步骤 6:代码优化(使用 Skills)

在开发完成后,我们可以使用 Skills 来优化代码质量和性能。

6.1 安装所需 Skills

# 先安装 find-skills(如果还没安装)
npx skills add @vercel-labs/skills/find-skills -g -y

# 安装 React 最佳实践技能
npx skills add @vercel-labs/agent-skills/react-best-practices -g -y

# 安装调试技能(帮助排查问题)
npx skills add @obra/superpowers/systematic-debugging -g -y

6.2 使用 Skills 优化代码

告诉 Claude Code 使用 React 最佳实践优化:

帮我优化这个 Todo 应用的代码,使用 React 最佳实践

Skills 会自动加载并执行以下优化

优化项优化前优化后效果
React.memo每次父组件渲染,子组件都重渲染props 不变时不重渲染减少不必要的渲染
useMemo每次渲染都计算 stats依赖变化才重新计算避免重复计算
useCallback每次渲染都创建新函数依赖不变时复用函数避免子组件无谓重渲染
key 优化使用 index 作为 key使用稳定的 id更准确的 DOM 更新

6.3 优化后的代码示例

// 优化后的 useTodos.js
import { useState, useEffect, useMemo, useCallback } from 'react';

export function useTodos() {
  const [todos, setTodos] = useState(() => {
    const saved = localStorage.getItem('todos');
    return saved ? JSON.parse(saved) : [];
  });

  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  // 使用 useCallback 缓存函数,避免子组件不必要重渲染
  const addTodo = useCallback((text) => {
    if (!text.trim()) return;
    setTodos(prev => [
      ...prev,
      {
        id: Date.now(),
        text: text.trim(),
        completed: false,
        createdAt: new Date().toISOString()
      }
    ]);
  }, []);

  const toggleTodo = useCallback((id) => {
    setTodos(prev => prev.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, []);

  const deleteTodo = useCallback((id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);

  const clearCompleted = useCallback(() => {
    setTodos(prev => prev.filter(todo => !todo.completed));
  }, []);

  // 使用 useMemo 缓存计算结果
  const stats = useMemo(() => ({
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
    active: todos.filter(t => !t.completed).length
  }), [todos]);

  return { todos, addTodo, toggleTodo, deleteTodo, clearCompleted, stats };
}

6.4 优化验证

运行后检查:

  1. 打开浏览器开发者工具的 Performance 面板
  2. 添加几个待办事项
  3. 观察渲染性能是否有提升
  4. 确认功能正常工作

使用 systematic-debugging 排查问题

如果遇到问题,可以使用:

我的程序出现问题了,请帮我调试

这会加载 systematic-debugging 技能,指导你系统化地排查问题。


🌤️ 案例二:天气查询应用(25 分钟)

学习目标

  • 掌握 API 调用与异步数据处理
  • 学习加载状态与错误处理
  • 理解 localStorage 在实际场景中的应用
  • 使用 MCP 连接外部天气 API
  • 使用 Skills 进行代码审查

步骤 1:项目初始化

# 创建项目
npm create vite@latest weather-app -- --template react
cd weather-app

# 安装依赖(axios 用于 HTTP 请求)
npm install axios

# 启动项目
npm run dev

步骤 2:启动 Claude Code 开发

帮我创建一个天气查询应用,功能要求:

1. 城市搜索:
   - 输入城市名搜索天气
   - 按回车或点击按钮搜索
   - 输入验证(非空检查)

2. 天气展示:
   - 当前温度(大字显示)
   - 体感温度
   - 天气状况(文字 + 图标)
   - 湿度百分比
   - 风速
   - PM2.5 指数(如果有)
   - 数据更新时间

3. 城市收藏:
   - 点击星标收藏当前城市
   - 收藏列表显示在页面底部
   - 点击收藏城市直接查看天气
   - 点击取消收藏

4. 数据持久化:
   - 收藏列表保存到 localStorage
   - 记住上次查看的城市

5. 加载与错误:
   - 搜索时显示加载动画
   - 网络错误显示友好提示
   - 城市不存在提示

6. 响应式:
   - 手机端单列布局
   - 电脑端卡片布局

使用和风天气 API,免费注册获取 Key:https://dev.qweather.com/
样式要求:清新简洁的主题,使用 CSS 实现,不需要额外库。

步骤 3:查看生成的文件结构

src/
├── App.jsx
├── main.jsx
├── api/
│   └── weather.js        # API 调用封装
├── components/
│   ├── SearchBox.jsx    # 搜索框
│   ├── WeatherDisplay.jsx  # 天气展示
│   ├── WeatherCard.jsx  # 天气卡片
│   ├── Favorites.jsx    # 收藏列表
│   └── Loading.jsx      # 加载组件
├── hooks/
│   └── useWeather.js    # 天气数据 Hook
├── utils/
│   └── storage.js       # localStorage 工具
├── App.css
└── index.css

步骤 4:核心代码展示

api/weather.js(API 封装)

import axios from 'axios';

// 和风天气 API
// 请替换为你的 API Key:https://dev.qweather.com/
const API_KEY = 'YOUR_API_KEY'; 
const BASE_URL = 'https://devapi.qweather.com/v7';

export async function getWeather(location) {
  try {
    const response = await axios.get(`${BASE_URL}/weather/now`, {
      params: {
        location,
        key: API_KEY
      }
    });
    
    if (response.data.code === '200') {
      return {
        success: true,
        data: response.data
      };
    } else {
      return {
        success: false,
        error: '城市未找到,请检查城市名称'
      };
    }
  } catch (error) {
    if (error.response?.status === 429) {
      return {
        success: false,
        error: 'API 请求次数超限,请稍后再试'
      };
    }
    return {
      success: false,
      error: '网络错误,请检查网络连接'
    };
  }
}

// 获取空气数据
export async function getAirQuality(location) {
  try {
    const response = await axios.get(`${BASE_URL}/air/now`, {
      params: {
        location,
        key: API_KEY
      }
    });
    
    if (response.data.code === '200') {
      return response.data.now;
    }
    return null;
  } catch {
    return null;
  }
}

useWeather.js(天气 Hook)

import { useState, useCallback } from 'react';
import { getWeather, getAirQuality } from '../api/weather';

export function useWeather() {
  const [weather, setWeather] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [favorites, setFavorites] = useState(() => {
    const saved = localStorage.getItem('weather-favorites');
    return saved ? JSON.parse(saved) : [];
  });

  // 保存收藏到 localStorage
  const saveFavorites = (newFavorites) => {
    setFavorites(newFavorites);
    localStorage.setItem('weather-favorites', JSON.stringify(newFavorites));
  };

  // 搜索天气
  const searchWeather = useCallback(async (city) => {
    if (!city.trim()) {
      setError('请输入城市名称');
      return;
    }

    setLoading(true);
    setError(null);

    const result = await getWeather(city);
    
    if (result.success) {
      // 获取空气质量
      const airData = await getAirQuality(city);
      
      setWeather({
        location: city,
        now: result.data.now,
        updateTime: result.data.updateTime,
        air: airData
      });
    } else {
      setError(result.error);
      setWeather(null);
    }

    setLoading(false);
  }, []);

  // 切换收藏
  const toggleFavorite = useCallback((city) => {
    const isFavorited = favorites.includes(city);
    if (isFavorited) {
      saveFavorites(favorites.filter(f => f !== city));
    } else {
      saveFavorites([...favorites, city]);
    }
  }, [favorites]);

  // 检查是否已收藏
  const isFavorited = useCallback((city) => {
    return favorites.includes(city);
  }, [favorites]);

  // 移除收藏
  const removeFavorite = useCallback((city) => {
    saveFavorites(favorites.filter(f => f !== city));
  }, [favorites]);

  return {
    weather,
    loading,
    error,
    favorites,
    searchWeather,
    toggleFavorite,
    isFavorited,
    removeFavorite
  };
}

App.jsx(主组件)

import { useState } from 'react';
import { useWeather } from './hooks/useWeather';
import SearchBox from './components/SearchBox';
import WeatherDisplay from './components/WeatherDisplay';
import Favorites from './components/Favorites';
import './App.css';

function App() {
  const {
    weather,
    loading,
    error,
    favorites,
    searchWeather,
    toggleFavorite,
    isFavorited,
    removeFavorite
  } = useWeather();

  const [searchCity, setSearchCity] = useState('');

  const handleSearch = () => {
    searchWeather(searchCity);
  };

  const handleKeyPress = (e) => {
    if (e.key === 'Enter') {
      handleSearch();
    }
  };

  return (
    <div className="app">
      <div className="container">
        <header className="header">
          <h1>🌤️ 天气查询</h1>
          <p className="subtitle">实时天气,一目了然</p>
        </header>

        <SearchBox
          value={searchCity}
          onChange={setSearchCity}
          onSearch={handleSearch}
          onKeyPress={handleKeyPress}
          loading={loading}
        />

        {loading && <div className="loading">加载中...</div>}
        
        {error && <div className="error">{error}</div>}

        {weather && (
          <WeatherDisplay
            weather={weather}
            isFavorited={isFavorited(weather.location)}
            onToggleFavorite={() => toggleFavorite(weather.location)}
          />
        )}

        {favorites.length > 0 && (
          <Favorites
            favorites={favorites}
            onSelect={(city) => {
              setSearchCity(city);
              searchWeather(city);
            }}
            onRemove={removeFavorite}
            currentCity={weather?.location}
          />
        )}
      </div>
    </div>
  );
}

export default App;

SearchBox.jsx(搜索组件)

import './SearchBox.css';

export default function SearchBox({ value, onChange, onSearch, onKeyPress, loading }) {
  return (
    <div className="search-box">
      <input
        type="text"
        value={value}
        onChange={(e) => onChange(e.target.value)}
        onKeyPress={onKeyPress}
        placeholder="请输入城市名称,如:北京、上海"
        className="search-input"
        disabled={loading}
      />
      <button
        onClick={onSearch}
        className="search-btn"
        disabled={loading}
      >
        {loading ? '查询中...' : '搜索'}
      </button>
    </div>
  );
}

WeatherDisplay.jsx(天气展示)

import './WeatherDisplay.css';

// 天气图标映射
const weatherIcons = {
  '晴': '☀️',
  '多云': '⛅',
  '阴': '☁️',
  '雨': '🌧️',
  '雪': '❄️',
  '雷': '⛈️',
  '雾': '🌫️',
  '沙': '🌪️'
};

export default function WeatherDisplay({ weather, isFavorited, onToggleFavorite }) {
  const { location, now, updateTime, air } = weather;
  const icon = weatherIcons[now.text] || '🌤️';

  return (
    <div className="weather-display">
      <div className="weather-header">
        <div className="location-box">
          <h2>{location}</h2>
          <span className="update-time">更新时间:{updateTime}</span>
        </div>
        <button
          className={`favorite-btn ${isFavorited ? 'active' : ''}`}
          onClick={onToggleFavorite}
        >
          {isFavorited ? '⭐ 已收藏' : '☆ 收藏'}
        </button>
      </div>

      <div className="weather-main">
        <div className="temp-box">
          <span className="temp">{now.temp}</span>
          <span className="unit">°C</span>
        </div>
        <div className="weather-info">
          <p className="condition">
            <span className="icon">{icon}</span>
            {now.text}
          </p>
          <p className="feels-like">体感温度:{now.feelsLike}°C</p>
        </div>
      </div>

      <div className="weather-details">
        <div className="detail-item">
          <span className="label">💧 湿度</span>
          <span className="value">{now.humidity}%</span>
        </div>
        <div className="detail-item">
          <span className="label">💨 风速</span>
          <span className="value">{now.windSpeed} km/h</span>
        </div>
        <div className="detail-item">
          <span className="label">🌡️ 气压</span>
          <span className="value">{now.pressure} hPa</span>
        </div>
        {air && (
          <div className="detail-item">
            <span className="label">🌫️ AQI</span>
            <span className="value">{air.aqi} {air.category}</span>
          </div>
        )}
      </div>
    </div>
  );
}

App.css(完整样式)

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: linear-gradient(180deg, #1e3c72 0%, #2a5298 100%);
  min-height: 100vh;
  padding: 20px;
}

.app {
  max-width: 480px;
  margin: 0 auto;
}

.container {
  background: rgba(255, 255, 255, 0.95);
  border-radius: 20px;
  padding: 24px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}

.header {
  text-align: center;
  margin-bottom: 24px;
}

.header h1 {
  color: #1e3c72;
  font-size: 28px;
}

.subtitle {
  color: #666;
  font-size: 14px;
  margin-top: 4px;
}

/* 搜索框 */
.search-box {
  display: flex;
  gap: 12px;
  margin-bottom: 20px;
}

.search-input {
  flex: 1;
  padding: 14px 16px;
  border: 2px solid #e0e0e0;
  border-radius: 12px;
  font-size: 16px;
  transition: border-color 0.2s;
}

.search-input:focus {
  outline: none;
  border-color: #2a5298;
}

.search-btn {
  padding: 14px 24px;
  background: #2a5298;
  color: white;
  border: none;
  border-radius: 12px;
  font-size: 16px;
  cursor: pointer;
}

.search-btn:disabled {
  background: #999;
  cursor: not-allowed;
}

/* 加载 */
.loading {
  text-align: center;
  padding: 20px;
  color: #666;
}

/* 错误 */
.error {
  background: #fff2f0;
  color: #ff4d4f;
  padding: 12px;
  border-radius: 8px;
  margin-bottom: 16px;
  text-align: center;
}

/* 天气展示 */
.weather-display {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 16px;
  padding: 20px;
  color: white;
  margin-bottom: 20px;
}

.weather-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 16px;
}

.location-box h2 {
  font-size: 24px;
}

.update-time {
  font-size: 12px;
  opacity: 0.8;
}

.favorite-btn {
  padding: 8px 12px;
  background: rgba(255, 255, 255, 0.2);
  border: none;
  border-radius: 8px;
  color: white;
  cursor: pointer;
  font-size: 14px;
}

.favorite-btn.active {
  background: rgba(255, 215, 0, 0.3);
}

.weather-main {
  display: flex;
  align-items: center;
  gap: 20px;
  margin-bottom: 20px;
}

.temp-box {
  display: flex;
  align-items: flex-start;
}

.temp {
  font-size: 64px;
  font-weight: bold;
}

.unit {
  font-size: 24px;
  margin-top: 8px;
}

.condition {
  font-size: 20px;
  margin-bottom: 8px;
}

.condition .icon {
  margin-right: 8px;
}

.feels-like {
  font-size: 14px;
  opacity: 0.8;
}

.weather-details {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
}

.detail-item {
  background: rgba(255, 255, 255, 0.15);
  padding: 12px;
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.detail-item .label {
  font-size: 12px;
  opacity: 0.8;
  margin-bottom: 4px;
}

.detail-item .value {
  font-size: 16px;
  font-weight: bold;
}

/* 收藏列表 */
.favorites {
  margin-top: 20px;
}

.favorites h3 {
  color: #333;
  font-size: 16px;
  margin-bottom: 12px;
}

.favorites-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.favorite-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  background: #f5f5f5;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.2s;
}

.favorite-item:hover {
  background: #e0e0e0;
}

.favorite-item.current {
  background: #e6f7ff;
  border: 1px solid #1890ff;
}

.remove-favorite {
  color: #999;
  cursor: pointer;
  font-size: 16px;
}

.remove-favorite:hover {
  color: #ff4d4f;
}

步骤 5:运行验证

  1. 访问 http://localhost:5173
  2. 输入城市名(如"北京"),按回车
  3. 等待加载完成,查看天气信息
  4. 点击收藏,刷新页面验证持久化
  5. 测试错误情况(如输入错误城市名)

步骤 6:使用 Skills 优化和 MCP 连接

6.1 使用 Skills 进行代码审查

天气应用涉及 API 调用、异步处理、错误处理,可以使用 Skills 来优化:

# 安装前端设计技能(优化 UI 代码)
npx skills add @anthropics/skills/frontend-design -g -y

# 安装 Web 设计指南(检查可访问性)
npx skills add @vercel-labs/agent-skills/web-design-guidelines -g -y

告诉 Claude Code 审查代码:

请审查这个天气应用的代码,检查:
1. 代码结构和组织
2. 错误处理是否完善
3. 性能是否有优化空间
4. 是否有安全漏洞(如 XSS)

Skills 会自动检查并给出优化建议

检查项问题建议修复
错误处理只捕获了部分错误添加更完善的错误类型判断
输入验证输入未做清理添加 sanitize 处理防止 XSS
API 限流无重试机制添加指数退避重试逻辑
缓存每次都请求 API添加缓存机制避免重复请求

6.2 使用 MCP 增强功能(可选)

如果需要更强大的天气数据,可以配置 MCP 服务器:

# 添加一个更专业的天气 MCP
# (需要自行查找支持天气数据的 MCP 服务器)
claude mcp add --transport http weather-api "https://api.example.com/mcp"

MCP vs 直接 API

  • 直接 API:适合简单场景,如和风天气 API
  • MCP:适合需要连接多个服务、或需要更复杂数据处理的场景

📊 案例三:数据仪表盘(45 分钟)

学习目标

  • 掌握复杂 UI 组件的拆分与组合
  • 学习图表库的使用(Recharts)
  • 理解数据可视化的设计原则
  • 练习复杂状态管理

步骤 1:项目初始化

# 创建项目
npm create vite@latest dashboard-app -- --template react
cd dashboard-app

# 安装图表库
npm install recharts

# 安装日期处理(可选)
npm install dayjs

# 启动项目
npm run dev

步骤 2:启动 Claude Code 开发

帮我创建一个销售数据仪表盘,功能要求:

1. 统计概览卡片(4个):
   - 总销售额(元,单位转换显示)
   - 订单总数
   - 平均客单价
   - 转化率(百分比)
   - 每个卡片显示同比变化(上升/下降箭头和颜色)

2. 销售趋势图(折线图):
   - 显示最近30天的每日销售额
   - X轴:日期
   - Y轴:销售额
   - 鼠标悬停显示详细数据

3. 品类分布(饼图):
   - 显示各产品品类的销售占比
   - 图例显示品类名称和百分比
   - 鼠标悬停显示具体金额

4. 区域排名(柱状图):
   - 显示各省份/城市的销售排名
   - 前10名
   - 水平柱状图

5. 热销产品(表格):
   - 产品名称
   - 销量
   - 销售额
   - 排名变化(上升/下降/持平)

6. 时间筛选:
   - 今天/昨天
   - 最近7天
   - 最近30天
   - 本月

7. 响应式设计:
   - 桌面端:2-3列布局
   - 平板端:2列布局
   - 手机端:单列堆叠

使用 Recharts 库实现图表。
模拟数据,包含30天的销售记录。
样式要求:深色主题专业仪表盘风格。

步骤 3:生成的文件结构

src/
├── App.jsx
├── main.jsx
├── data/
│   └── mockData.js      # 模拟数据
├── components/
│   ├── StatCard.jsx     # 统计卡片
│   ├── SalesTrendChart.jsx  # 销售趋势图
│   ├── CategoryPieChart.jsx # 品类饼图
│   ├── RegionBarChart.jsx   # 区域排名图
│   ├── ProductTable.jsx     # 产品表格
│   └── TimeFilter.jsx       # 时间筛选
├── hooks/
│   └── useDashboardData.js  # 数据处理 Hook
├── utils/
│   └── formatters.js    # 格式化工具函数
├── App.css
└── index.css

步骤 4:核心代码展示

mockData.js(模拟数据)

// 生成30天的日期
const generateDates = () => {
  const dates = [];
  const today = new Date();
  for (let i = 29; i >= 0; i--) {
    const date = new Date(today);
    date.setDate(date.getDate() - i);
    dates.push(date.toISOString().split('T')[0]);
  }
  return dates;
};

// 生成随机销售数据
const generateDailySales = (dates) => {
  return dates.map(date => {
    const baseSales = 8000 + Math.random() * 12000;
    const weekendMultiplier = [0, 6].includes(new Date(date).getDay()) ? 0.7 : 1;
    return {
      date,
      sales: Math.round(baseSales * weekendMultiplier),
      orders: Math.round(50 + Math.random() * 80)
    };
  });
};

// 产品数据
export const products = [
  { id: 1, name: 'iPhone 15 Pro', sales: 1250, revenue: 12487500, category: '数码' },
  { id: 2, name: 'MacBook Pro 14', sales: 890, revenue: 16020000, category: '数码' },
  { id: 3, name: 'AirPods Pro', sales: 3200, revenue: 4800000, category: '配件' },
  { id: 4, name: 'iPad Air', sales: 980, revenue: 4704000, category: '数码' },
  { id: 5, name: 'Apple Watch', sales: 1560, revenue: 8424000, category: '穿戴' },
  { id: 6, name: 'AirTag', sales: 2100, revenue: 630000, category: '配件' },
  { id: 7, name: 'MagSafe 充电器', sales: 1800, revenue: 414000, category: '配件' },
  { id: 8, name: 'HomePod mini', sales: 650, revenue: 975000, category: '家居' },
];

// 品类分布
export const categoryData = [
  { name: '数码', value: 37942500, color: '#667eea' },
  { name: '配件', value: 5844000, color: '#764ba2' },
  { name: '穿戴', value: 8424000, color: '#f093fb' },
  { name: '家居', value: 975000, color: '#4facfe' },
];

// 区域销售
export const regionData = [
  { region: '广东', sales: 15230000 },
  { region: '浙江', sales: 12850000 },
  { region: '江苏', sales: 9800000 },
  { region: '北京', sales: 8700000 },
  { region: '上海', sales: 7650000 },
  { region: '四川', sales: 5400000 },
  { region: '湖北', sales: 4200000 },
  { region: '山东', sales: 3800000 },
  { region: '福建', sales: 3200000 },
  { region: '河南', sales: 2800000 },
];

// 主数据生成
export const generateDashboardData = (dateRange = '30d') => {
  const dates = generateDates();
  const dailyData = generateDailySales(dates);
  
  // 计算统计指标
  const totalSales = dailyData.reduce((sum, d) => sum + d.sales, 0);
  const totalOrders = dailyData.reduce((sum, d) => sum + d.orders, 0);
  const avgOrderValue = Math.round(totalSales / totalOrders);
  const conversionRate = (Math.random() * 2 + 2).toFixed(2); // 2-4% 之间
  
  // 同比变化(模拟数据)
  const changes = {
    sales: (Math.random() * 30 - 10).toFixed(1),
    orders: (Math.random() * 25 - 5).toFixed(1),
    avgOrder: (Math.random() * 20 - 8).toFixed(1),
    conversion: (Math.random() * 1 - 0.5).toFixed(2)
  };

  return {
    stats: {
      totalSales,
      totalOrders,
      avgOrderValue,
      conversionRate,
      changes
    },
    dailyData,
    products: products.sort((a, b) => b.revenue - a.revenue),
    categoryData,
    regionData: regionData.slice(0, 10)
  };
};

useDashboardData.js(数据 Hook)

import { useState, useMemo } from 'react';
import { generateDashboardData } from '../data/mockData';

// 格式化函数
export const formatNumber = (num) => {
  if (num >= 10000) {
    return (num / 10000).toFixed(1) + '万';
  }
  return num.toLocaleString();
};

export const formatCurrency = (num) => {
  if (num >= 100000000) {
    return (num / 100000000).toFixed(2) + '亿';
  }
  if (num >= 10000) {
    return (num / 10000).toFixed(1) + '万';
  }
  return '¥' + num.toLocaleString();
};

export function useDashboardData() {
  const [dateRange, setDateRange] = useState('30d');
  const [refreshing, setRefreshing] = useState(false);

  const data = useMemo(() => {
    return generateDashboardData(dateRange);
  }, [dateRange]);

  // 刷新数据
  const refreshData = () => {
    setRefreshing(true);
    setTimeout(() => {
      setRefreshing(false);
    }, 1000);
  };

  return {
    data,
    dateRange,
    setDateRange,
    refreshing,
    refreshData,
    formatCurrency,
    formatNumber
  };
}

App.jsx(主组件)

import { useDashboardData } from './hooks/useDashboardData';
import StatCard from './components/StatCard';
import SalesTrendChart from './components/SalesTrendChart';
import CategoryPieChart from './components/CategoryPieChart';
import RegionBarChart from './components/RegionBarChart';
import ProductTable from './components/ProductTable';
import TimeFilter from './components/TimeFilter';
import './App.css';

function App() {
  const {
    data,
    dateRange,
    setDateRange,
    refreshing,
    refreshData,
    formatCurrency,
    formatNumber
  } = useDashboardData();

  const { stats, dailyData, products, categoryData, regionData } = data;

  return (
    <div className="dashboard">
      <div className="dashboard-header">
        <div className="header-left">
          <h1>📊 销售数据仪表盘</h1>
          <p className="subtitle">实时掌握业务动态</p>
        </div>
        <div className="header-right">
          <TimeFilter value={dateRange} onChange={setDateRange} />
          <button className="refresh-btn" onClick={refreshData} disabled={refreshing}>
            {refreshing ? '🔄 刷新中...' : '🔄 刷新数据'}
          </button>
        </div>
      </div>

      <div className="stats-grid">
        <StatCard
          title="总销售额"
          value={formatCurrency(stats.totalSales)}
          change={stats.changes.sales}
          icon="💰"
          color="#667eea"
        />
        <StatCard
          title="订单总数"
          value={formatNumber(stats.totalOrders)}
          change={stats.changes.orders}
          icon="📦"
          color="#764ba2"
        />
        <StatCard
          title="客单价"
          value={formatCurrency(stats.avgOrderValue)}
          change={stats.changes.avgOrder}
          icon="🛒"
          color="#f093fb"
        />
        <StatCard
          title="转化率"
          value={stats.conversionRate + '%'}
          change={stats.changes.conversion}
          icon="📈"
          color="#4facfe"
        />
      </div>

      <div className="charts-grid">
        <div className="chart-card full-width">
          <h3>📈 销售趋势</h3>
          <SalesTrendChart data={dailyData} />
        </div>
        
        <div className="chart-card">
          <h3>🥧 品类分布</h3>
          <CategoryPieChart data={categoryData} formatCurrency={formatCurrency} />
        </div>
        
        <div className="chart-card">
          <h3>🌍 区域排名</h3>
          <RegionBarChart data={regionData} formatCurrency={formatCurrency} />
        </div>
      </div>

      <div className="table-card">
        <h3>🏆 热销产品</h3>
        <ProductTable products={products} formatCurrency={formatCurrency} formatNumber={formatNumber} />
      </div>
    </div>
  );
}

export default App;

SalesTrendChart.jsx(趋势图)

import {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  ResponsiveContainer,
  Area,
  AreaChart
} from 'recharts';

const CustomTooltip = ({ active, payload, label }) => {
  if (active && payload && payload.length) {
    return (
      <div className="custom-tooltip">
        <p className="date">{label}</p>
        <p className="sales">销售额: ¥{payload[0].value.toLocaleString()}</p>
        <p className="orders">订单数: {payload[1].value}</p>
      </div>
    );
  }
  return null;
};

export default function SalesTrendChart({ data }) {
  // 格式化日期显示
  const chartData = data.map(item => ({
    ...item,
    displayDate: item.date.slice(5) // 只显示月-日
  }));

  return (
    <ResponsiveContainer width="100%" height={300}>
      <AreaChart data={chartData} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
        <defs>
          <linearGradient id="colorSales" x1="0" y1="0" x2="0" y2="1">
            <stop offset="5%" stopColor="#667eea" stopOpacity={0.8}/>
            <stop offset="95%" stopColor="#667eea" stopOpacity={0}/>
          </linearGradient>
        </defs>
        <CartesianGrid strokeDasharray="3 3" stroke="#374151" />
        <XAxis 
          dataKey="displayDate" 
          stroke="#9ca3af"
          tick={{ fontSize: 12 }}
        />
        <YAxis 
          stroke="#9ca3af"
          tick={{ fontSize: 12 }}
          tickFormatter={(value) => value >= 1000 ? (value/1000).toFixed(0) + 'k' : value}
        />
        <Tooltip content={<CustomTooltip />} />
        <Area
          type="monotone"
          dataKey="sales"
          stroke="#667eea"
          fillOpacity={1}
          fill="url(#colorSales)"
          strokeWidth={2}
        />
      </AreaChart>
    </ResponsiveContainer>
  );
}

App.css(完整样式)

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #0f172a;
  min-height: 100vh;
  padding: 20px;
  color: #e5e7eb;
}

.dashboard {
  max-width: 1400px;
  margin: 0 auto;
}

/* 头部 */
.dashboard-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
  flex-wrap: wrap;
  gap: 16px;
}

.header-left h1 {
  font-size: 28px;
  color: #fff;
}

.subtitle {
  color: #9ca3af;
  font-size: 14px;
  margin-top: 4px;
}

.header-right {
  display: flex;
  gap: 12px;
}

.refresh-btn {
  padding: 10px 16px;
  background: #3b82f6;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-size: 14px;
}

.refresh-btn:hover {
  background: #2563eb;
}

.refresh-btn:disabled {
  background: #6b7280;
  cursor: not-allowed;
}

/* 时间筛选 */
.time-filter {
  display: flex;
  background: #1f2937;
  border-radius: 8px;
  padding: 4px;
}

.time-filter button {
  padding: 8px 16px;
  background: transparent;
  color: #9ca3af;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.2s;
}

.time-filter button.active {
  background: #3b82f6;
  color: white;
}

.time-filter button:hover:not(.active) {
  color: #fff;
}

/* 统计卡片 */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px;
  margin-bottom: 24px;
}

.stat-card {
  background: #1f2937;
  border-radius: 12px;
  padding: 20px;
  border: 1px solid #374151;
}

.stat-card .header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
}

.stat-card .title {
  color: #9ca3af;
  font-size: 14px;
}

.stat-card .icon {
  font-size: 24px;
}

.stat-card .value {
  font-size: 32px;
  font-weight: bold;
  color: #fff;
  margin-bottom: 8px;
}

.stat-card .change {
  font-size: 14px;
  display: flex;
  align-items: center;
  gap: 4px;
}

.stat-card .change.positive {
  color: #10b981;
}

.stat-card .change.negative {
  color: #ef4444;
}

/* 图表网格 */
.charts-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
  margin-bottom: 24px;
}

.chart-card {
  background: #1f2937;
  border-radius: 12px;
  padding: 20px;
  border: 1px solid #374151;
}

.chart-card.full-width {
  grid-column: 1 / -1;
}

.chart-card h3 {
  color: #fff;
  font-size: 16px;
  margin-bottom: 16px;
}

/* 产品表格 */
.table-card {
  background: #1f2937;
  border-radius: 12px;
  padding: 20px;
  border: 1px solid #374151;
}

.table-card h3 {
  color: #fff;
  font-size: 16px;
  margin-bottom: 16px;
}

.table-wrapper {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
}

th {
  text-align: left;
  padding: 12px;
  color: #9ca3af;
  font-weight: 500;
  border-bottom: 1px solid #374151;
  font-size: 14px;
}

td {
  padding: 12px;
  border-bottom: 1px solid #374151;
  color: #e5e7eb;
  font-size: 14px;
}

tr:hover td {
  background: #374151;
}

.rank {
  display: inline-block;
  width: 24px;
  height: 24px;
  background: #3b82f6;
  border-radius: 6px;
  text-align: center;
  line-height: 24px;
  font-size: 12px;
  color: white;
}

.rank.top-3 {
  background: linear-gradient(135deg, #f59e0b, #d97706);
}

/* Tooltip */
.custom-tooltip {
  background: #1f2937;
  border: 1px solid #374151;
  border-radius: 8px;
  padding: 12px;
}

.custom-tooltip .date {
  color: #9ca3af;
  font-size: 12px;
  margin-bottom: 8px;
}

.custom-tooltip .sales {
  color: #667eea;
  font-weight: bold;
}

.custom-tooltip .orders {
  color: #9ca3af;
  font-size: 12px;
}

/* 响应式 */
@media (max-width: 768px) {
  .charts-grid {
    grid-template-columns: 1fr;
  }
  
  .chart-card.full-width {
    grid-column: auto;
  }
  
  .header-right {
    width: 100%;
    flex-direction: column;
  }
  
  .time-filter {
    width: 100%;
    justify-content: space-between;
  }
}

步骤 5:运行验证

  1. 访问 http://localhost:5173
  2. 查看四个统计卡片的数据
  3. 悬停在趋势图上查看详细数据
  4. 点击饼图扇区查看品类详情
  5. 使用时间筛选切换不同周期
  6. 点击刷新按钮测试数据更新

步骤 6:使用 Skills 优化和 MCP 连接

6.1 使用 Skills 优化仪表盘

仪表盘涉及图表可视化、复杂状态管理,使用 Skills 可以提升代码质量:

# 安装 React 最佳实践(优化组件性能)
npx skills add @vercel-labs/agent-skills/react-best-practices -g -y

# 安装组件组合模式(优化组件 API 设计)
npx skills add @vercel-labs/agent-skills/composition-patterns -g -y

告诉 Claude Code 优化代码:

帮我优化这个仪表盘代码,使用 React 最佳实践,特别关注:
1. 图表组件的性能优化
2. 大量数据的渲染优化
3. 组件之间的状态管理优化

Skills 会自动应用以下优化

优化项优化前优化后效果
数据分页一次渲染 30 天数据使用虚拟滚动减少 DOM 节点
图表懒加载页面加载时渲染所有图表使用 React.lazy减少首屏加载时间
数据缓存每次切换都重新请求使用 useMemo 缓存避免重复计算
组件拆分大组件包含所有逻辑拆分为小组件提高可维护性

6.2 使用 MCP 连接真实数据源

对于真实项目,可以使用 MCP 连接数据库获取真实数据:

# 连接 PostgreSQL 数据库
claude mcp add --transport stdio postgres -- npx -y @modelcontextprotocol/server-postgres "postgresql://user:pass@localhost:5432/sales"

# 连接 Supabase(可选)
claude mcp add --transport http supabase "https://mcp.supabase.com/mcp"

配置后,告诉 Claude Code 使用 MCP 数据:

请修改这个仪表盘,使用 MCP 连接 PostgreSQL 数据库获取真实销售数据:
- 从 sales 表获取每日销售数据
- 从 products 表获取产品信息
- 从 orders 表获取订单数据

6.3 常用 MCP 服务器推荐

MCP 服务器用途配置示例
PostgreSQL查询销售数据claude mcp add --transport stdio postgres -- npx -y @modelcontextprotocol/server-postgres "postgresql://..."
GitHub获取项目信息claude mcp add --transport http github https://api.githubcopilot.com/mcp/
Sentry获取错误数据claude mcp add --transport http sentry https://mcp.sentry.dev/mcp
Slack发送报告claude mcp add --transport http slack https://mcp.slack.com/mcp

使用场景

  • 定时发送报告:配置 Slack MCP,每天下午 5 点自动发送仪表盘数据到 #sales 频道
  • 实时监控:配置 Sentry MCP,当错误数超过阈值时自动在仪表盘显示告警
  • 自动化数据同步:配置数据库 MCP,每天凌晨自动同步昨日销售数据

🎯 二、开发流程总结

标准开发流程

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  需求确认   │ →  │  项目初始化  │ →  │  功能开发   │
└─────────────┘    └─────────────┘    └─────────────┘
       ↓                                           ↓
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  测试优化   │ ←  │  代码审查   │ ←  │  增量开发   │
└─────────────┘    └─────────────┘    └─────────────┘

需求确认要点

要素示例说明
功能列表添加、删除、编辑、搜索明确要实现的功能
技术栈React + Vite + TypeScript指定框架和工具
数据方案localStorage / API / Mock数据从哪里来
UI 要求深色主题、响应式设计风格和适配
特殊情况加载状态、错误处理、空状态边界情况处理

分步开发原则

第1步:创建项目骨架
       └── npm create vite + 基本结构

第2步:实现核心功能
       └── 从最重要的功能开始

第3步:完善交互细节
       └── 加载、错误、空状态

第4步:优化代码
       └── 使用 Skills 审查和优化

第5步:测试验证
       └── 实际运行确保功能正确

💡 三、实战技巧

技巧一:清晰的需求描述

❌ 帮我做个应用
✅ 帮我做一个 Todo 应用,需要:
   - 使用 React + Vite
   - 可以添加、删除、标记完成待办
   - 用 localStorage 保存数据
   - 简洁的 UI 样式

技巧二:分步开发

❌ 一次完成所有功能
   → 代码量大,容易出错

✅ 分步开发
   Step 1: 先完成项目初始化和基础结构
   Step 2: 添加核心功能
   Step 3: 完善细节和优化

技巧三:使用 Skills 优化

# 安装 React 性能优化技能
npx skills add @vercel-labs/agent-skills/react-best-practices -g -y

# 安装调试技能
npx skills add @obra/superpowers/systematic-debugging -g -y

# 安装代码审查技能
npx skills add @vercel-labs/agent-skills/code-review -g -y

技巧四:及时代码审查

$ claude "代码写完了,帮我检查一下有什么问题"

Claude Code 会:
- 检查潜在的 bug
- 识别性能问题
- 发现安全漏洞
- 提出优化建议

技巧五:正确使用 MCP

# 安装 MCP 服务器连接外部服务
claude mcp add --transport http github https://api.githubcopilot.com/mcp/

# 查看已安装的 MCP
claude mcp list

# 在项目中请求 MCP 服务
$ claude "帮我从数据库查询最近一个月的销售数据"

技巧六:结合 Skills 和 MCP

# 在复杂项目中使用
1. MCP:连接外部数据源(数据库、API)
2. Skills:指导 AI 按最佳实践处理数据
3. 效果:既能得到数据,又能保证代码质量

示例:仪表盘项目
- MCP (PostgreSQL) → 获取真实销售数据
- Skills (React Best Practices) → 按最佳实践渲染图表
- MCP (Sentry) → 实时获取错误监控数据

⌨️ 四、项目开发命令流程

完整项目开发命令

# 1. 创建项目
claude "创建 React + TypeScript 项目"

# 2. 进入目录
cd my-project

# 3. 安装依赖
claude "安装 axios recharts"

# 4. 开发功能
claude "帮我创建用户列表组件"

# 5. 代码审查
claude --model opus-4-6 "审查代码并修复问题"

# 6. 测试
claude "生成单元测试"

# 7. 提交
claude "初始化 git 并创建首次提交"

会话内常用命令

命令功能使用场景
/use opus-4-6切换到最强模型复杂代码审查
/clear清除历史开始新功能
/compact压缩上下文节省 tokens
/resume继续之前工作中断后继续
/branch创建分支尝试不同方案
/plan系统规划大型项目规划

🛠️ 五、常见问题解决

Q1:生成的代码有 bug 怎么办?

A:

  1. 直接告诉错误:将完整的错误信息粘贴给 Claude Code
  2. 描述问题现象:说明期望行为和实际行为
  3. 请求修复:让 AI 分析原因并修复
出现了这个错误:[粘贴错误信息]
期望是 XXX,但实际是 XXX
请帮我修复

Q2:代码不符合项目规范怎么办?

A:

  1. 提供参考代码:将现有代码风格作为参考
  2. 明确规范要求:说明具体的命名、结构要求
  3. 批量调整:让 AI 统一调整
请按照这个项目的代码风格调整:
- 使用函数式组件
- 组件放在 components 目录
- 使用 useMemo 优化计算

Q3:功能太复杂怎么办?

A:

  1. 拆分任务:将复杂功能拆成多个小任务
  2. 使用规划:使用 /plan 进行系统规划
  3. 分支尝试:使用 /branch 尝试不同方案
这个功能比较大,请先帮我规划一下:
1. 需要哪些模块
2. 开发的先后顺序
3. 可能的难点

Q4:API Key 如何处理?

A:

  1. 使用环境变量:不要硬编码在代码中
  2. 创建 .env 文件:存储敏感信息
  3. 添加到 .gitignore:确保不提交到 Git
# .env 文件
VITE_WEATHER_API_KEY=your_key_here

# 在代码中使用
import.meta.env.VITE_WEATHER_API_KEY

Q5:项目开发中什么时候用 MCP?什么时候用 Skills?

A:

场景使用 MCP使用 Skills
获取外部数据✅ 连接数据库、API、GitHub❌ 不适用
提升代码质量❌ 不适用✅ 代码审查、性能优化
自动化操作✅ 自动获取 Sentry 错误❌ 不适用
规范开发流程❌ 不适用✅ TDD、调试方法论
连接多个服务✅ 一次配置,多次使用❌ 不适用

最佳实践:MCP 和 Skills 结合使用

MCP (数据层) + Skills (逻辑层) = 完整解决方案

示例:
- MCP 连接 PostgreSQL 获取销售数据
- Skills 指导按 React 最佳实践渲染图表
- 两者配合,代码既有数据又有质量

Q6:如何在项目中使用多个 MCP 服务器?

A:

  1. 逐一安装
claude mcp add --transport http github https://api.githubcopilot.com/mcp/
claude mcp add --transport http sentry https://mcp.sentry.dev/mcp
claude mcp add --transport stdio postgres -- npx -y @modelcontextprotocol/server-postgres "postgresql://..."
  1. 查看状态
claude mcp list
  1. 在项目中使用
"从 GitHub 获取最近 commits,从 Sentry 获取最近 errors,从数据库获取销售数据,整合成一个报告"

注意

MCP 服务器越多,上下文消耗越大。建议开启 Tool Search:

ENABLE_TOOL_SEARCH=auto claude

📝 六、练习作业

[!example]>
动手练习:尝试用 Claude Code 完成以下项目:

难度项目预计时间学习重点
计算器10 分钟基础交互、状态计算
⭐⭐记事本20 分钟CRUD 操作、localStorage
⭐⭐⭐博客系统60 分钟前端+后端、API 设计

练习提示

  1. 计算器:实现基本的加减乘除运算,支持键盘输入
  2. 记事本:增删改查功能,支持 Markdown 预览
  3. 博客系统:文章列表、详情页、评论功能

🎉 总结

阶段要点
需求确认说清楚要什么,包括功能、技术栈、UI 要求
分步开发从小处着手,先完成基础再完善细节
及时审查使用 Skills 避免问题累积
测试验证实际运行确保功能正确
迭代优化根据运行效果持续改进
数据获取使用 MCP 连接外部数据源
质量保证使用 Skills 优化代码质量

本期案例使用的 MCP 和 Skills

案例使用的 MCP使用的 Skills完成功能
Todo 应用React Best Practices代码性能优化
天气应用无(直接 API)Frontend Design, Web Design Guidelines代码审查、UI 优化
数据仪表盘PostgreSQL(可选)React Best Practices, Composition Patterns真实数据连接、组件优化

推荐安装的 Skills 和 MCP

# 必装 Skills
npx skills add @vercel-labs/skills/find-skills -g -y
npx skills add @vercel-labs/agent-skills/react-best-practices -g -y
npx skills add @obra/superpowers/systematic-debugging -g -y
npx skills add @obra/superpowers/brainstorming -g -y

# 推荐 MCP
claude mcp add --transport http github https://api.githubcopilot.com/mcp/

[!success]>
下期预告

第七篇我们将讲解 Sub-Agents 与团队协作,教你如何用 Claude Code 完成复杂任务!

敬请期待!