写在前面
前五篇我们学习了 Claude Code 的基础知识、提示词技巧、代码生成、文件操作和 MCP/Skills。今天终于到了实战环节!我们将用三个完整的项目案例,展示如何从零开始使用 Claude Code 开发真实项目。
特别说明:本文提供的是可以直接复制使用的完整代码,每个案例都经过验证,确保可以实际运行。
🎯 一、实战概览
本期案例
| 案例 | 难度 | 技术栈 | 时长 | 学习重点 |
|---|---|---|---|---|
| 📝 Todo 待办应用 | ⭐ | React + localStorage | 15 分钟 | 基础交互、状态管理 |
| 🌤️ 天气查询应用 | ⭐⭐ | React + API | 25 分钟 | API 调用、异步处理 |
| 📊 数据仪表盘 | ⭐⭐⭐ | React + Recharts | 45 分钟 | 图表可视化、复杂状态 |
实战学习路径
第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:验证运行
- 打开浏览器访问
http://localhost:5173 - 尝试添加几个待办事项
- 标记几个为完成
- 删除一些
- 刷新页面,确认数据仍然保留
步骤 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 优化验证
运行后检查:
- 打开浏览器开发者工具的 Performance 面板
- 添加几个待办事项
- 观察渲染性能是否有提升
- 确认功能正常工作
使用 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:运行验证
- 访问
http://localhost:5173 - 输入城市名(如"北京"),按回车
- 等待加载完成,查看天气信息
- 点击收藏,刷新页面验证持久化
- 测试错误情况(如输入错误城市名)
步骤 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:运行验证
- 访问
http://localhost:5173 - 查看四个统计卡片的数据
- 悬停在趋势图上查看详细数据
- 点击饼图扇区查看品类详情
- 使用时间筛选切换不同周期
- 点击刷新按钮测试数据更新
步骤 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:
- 直接告诉错误:将完整的错误信息粘贴给 Claude Code
- 描述问题现象:说明期望行为和实际行为
- 请求修复:让 AI 分析原因并修复
出现了这个错误:[粘贴错误信息]
期望是 XXX,但实际是 XXX
请帮我修复
Q2:代码不符合项目规范怎么办?
A:
- 提供参考代码:将现有代码风格作为参考
- 明确规范要求:说明具体的命名、结构要求
- 批量调整:让 AI 统一调整
请按照这个项目的代码风格调整:
- 使用函数式组件
- 组件放在 components 目录
- 使用 useMemo 优化计算
Q3:功能太复杂怎么办?
A:
- 拆分任务:将复杂功能拆成多个小任务
- 使用规划:使用 /plan 进行系统规划
- 分支尝试:使用 /branch 尝试不同方案
这个功能比较大,请先帮我规划一下:
1. 需要哪些模块
2. 开发的先后顺序
3. 可能的难点
Q4:API Key 如何处理?
A:
- 使用环境变量:不要硬编码在代码中
- 创建 .env 文件:存储敏感信息
- 添加到 .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:
- 逐一安装:
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://..."
- 查看状态:
claude mcp list
- 在项目中使用:
"从 GitHub 获取最近 commits,从 Sentry 获取最近 errors,从数据库获取销售数据,整合成一个报告"
注意
MCP 服务器越多,上下文消耗越大。建议开启 Tool Search:
ENABLE_TOOL_SEARCH=auto claude
📝 六、练习作业
[!example]>
动手练习:尝试用 Claude Code 完成以下项目:
| 难度 | 项目 | 预计时间 | 学习重点 |
|---|---|---|---|
| ⭐ | 计算器 | 10 分钟 | 基础交互、状态计算 |
| ⭐⭐ | 记事本 | 20 分钟 | CRUD 操作、localStorage |
| ⭐⭐⭐ | 博客系统 | 60 分钟 | 前端+后端、API 设计 |
练习提示
- 计算器:实现基本的加减乘除运算,支持键盘输入
- 记事本:增删改查功能,支持 Markdown 预览
- 博客系统:文章列表、详情页、评论功能
🎉 总结
| 阶段 | 要点 |
|---|---|
| 需求确认 | 说清楚要什么,包括功能、技术栈、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 完成复杂任务!
敬请期待!