src/
├── App.jsx
├── main.jsx
├── Counter.jsx
└── Counter.css
import { useState } from 'react';
import './Counter.css';
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
return (
<div className="counter">
<h1>计数器</h1>
<div className="display">{count}</div>
<div className="controls">
<button onClick={() => setCount(count - step)}>-{step}</button>
<button onClick={() => setCount(count + step)}>+{step}</button>
<button onClick={() => setCount(0)}>重置</button>
</div>
<div className="step-control">
<label>步长: </label>
<input
type="number"
value={step}
onChange={(e) => setStep(Number(e.target.value))}
min="1"
/>
</div>
</div>
);
}
export default Counter;
.counter {
max-width: 400px;
margin: 50px auto;
padding: 30px;
border: 2px solid #ddd;
border-radius: 10px;
text-align: center;
}
.display {
font-size: 72px;
font-weight: bold;
color: #333;
margin: 30px 0;
}
.controls button {
margin: 5px;
padding: 10px 20px;
font-size: 18px;
cursor: pointer;
border: none;
border-radius: 5px;
background: #007bff;
color: white;
}
.controls button:hover {
background: #0056b3;
}
.step-control {
margin-top: 20px;
}
.step-control input {
width: 60px;
padding: 5px;
font-size: 16px;
}
src/
├── App.jsx
├── main.jsx
├── TodoApp.jsx
└── TodoApp.css
- 添加待办
- 标记完成
- 删除待办
- 筛选(全部/未完成/已完成)
- 显示统计
import { useState } from 'react';
import './TodoApp.css';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const [filter, setFilter] = useState('all');
const addTodo = () => {
if (input.trim()) {
setTodos([
...todos,
{ id: Date.now(), text: input, completed: false }
]);
setInput('');
}
};
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 filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
const stats = {
total: todos.length,
active: todos.filter(t => !t.completed).length,
completed: todos.filter(t => t.completed).length
};
return (
<div className="todo-app">
<h1>待办事项</h1>
<div className="input-section">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="输入待办事项..."
/>
<button onClick={addTodo}>添加</button>
</div>
<div className="filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
全部 ({stats.total})
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
未完成 ({stats.active})
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
已完成 ({stats.completed})
</button>
</div>
<ul className="todo-list">
{filteredTodos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>删除</button>
</li>
))}
</ul>
{stats.completed > 0 && (
<button className="clear-btn" onClick={clearCompleted}>
清除已完成
</button>
)}
</div>
);
}
export default TodoApp;
.todo-app {
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.input-section {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.input-section input {
flex: 1;
padding: 10px;
font-size: 16px;
border: 2px solid #ddd;
border-radius: 5px;
}
.input-section button {
padding: 10px 20px;
font-size: 16px;
background: #28a745;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.filters button {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 5px;
}
.filters button.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
gap: 10px;
padding: 12px;
border: 1px solid #ddd;
margin-bottom: 8px;
border-radius: 5px;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #999;
}
.todo-list li span {
flex: 1;
}
.todo-list li button {
padding: 5px 10px;
background: #dc3545;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
.clear-btn {
width: 100%;
padding: 10px;
margin-top: 20px;
background: #6c757d;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
src/
├── App.jsx
├── main.jsx
├── RegisterForm.jsx
└── RegisterForm.css
import { useState } from 'react';
import './RegisterForm.css';
function RegisterForm() {
const [form, setForm] = useState({
username: '',
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validate = (name, value) => {
switch (name) {
case 'username':
if (!value) return '用户名不能为空';
if (value.length < 3) return '用户名至少 3 个字符';
return '';
case 'email':
if (!value) return '邮箱不能为空';
if (!/\S+@\S+\.\S+/.test(value)) return '邮箱格式不正确';
return '';
case 'password':
if (!value) return '密码不能为空';
if (value.length < 6) return '密码至少 6 个字符';
return '';
case 'confirmPassword':
if (!value) return '请确认密码';
if (value !== form.password) return '两次密码不一致';
return '';
default:
return '';
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setForm({ ...form, [name]: value });
if (touched[name]) {
const error = validate(name, value);
setErrors({ ...errors, [name]: error });
}
};
const handleBlur = (e) => {
const { name, value } = e.target;
setTouched({ ...touched, [name]: true });
const error = validate(name, value);
setErrors({ ...errors, [name]: error });
};
const handleSubmit = (e) => {
e.preventDefault();
const newErrors = {};
Object.keys(form).forEach(key => {
const error = validate(key, form[key]);
if (error) newErrors[key] = error;
});
setErrors(newErrors);
setTouched({
username: true,
email: true,
password: true,
confirmPassword: true
});
if (Object.keys(newErrors).length === 0) {
console.log('提交表单:', form);
alert('注册成功!');
setForm({
username: '',
email: '',
password: '',
confirmPassword: ''
});
setTouched({});
}
};
return (
<div className="register-form">
<h1>用户注册</h1>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>用户名</label>
<input
name="username"
value={form.username}
onChange={handleChange}
onBlur={handleBlur}
className={errors.username && touched.username ? 'error' : ''}
/>
{errors.username && touched.username && (
<span className="error-msg">{errors.username}</span>
)}
</div>
<div className="form-group">
<label>邮箱</label>
<input
name="email"
type="email"
value={form.email}
onChange={handleChange}
onBlur={handleBlur}
className={errors.email && touched.email ? 'error' : ''}
/>
{errors.email && touched.email && (
<span className="error-msg">{errors.email}</span>
)}
</div>
<div className="form-group">
<label>密码</label>
<input
name="password"
type="password"
value={form.password}
onChange={handleChange}
onBlur={handleBlur}
className={errors.password && touched.password ? 'error' : ''}
/>
{errors.password && touched.password && (
<span className="error-msg">{errors.password}</span>
)}
</div>
<div className="form-group">
<label>确认密码</label>
<input
name="confirmPassword"
type="password"
value={form.confirmPassword}
onChange={handleChange}
onBlur={handleBlur}
className={errors.confirmPassword && touched.confirmPassword ? 'error' : ''}
/>
{errors.confirmPassword && touched.confirmPassword && (
<span className="error-msg">{errors.confirmPassword}</span>
)}
</div>
<button type="submit">注册</button>
</form>
</div>
);
}
export default RegisterForm;
.register-form {
max-width: 400px;
margin: 50px auto;
padding: 30px;
border: 1px solid #ddd;
border-radius: 10px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input {
width: 100%;
padding: 10px;
font-size: 16px;
border: 2px solid #ddd;
border-radius: 5px;
box-sizing: border-box;
}
.form-group input.error {
border-color: #dc3545;
}
.error-msg {
display: block;
color: #dc3545;
font-size: 14px;
margin-top: 5px;
}
button[type="submit"] {
width: 100%;
padding: 12px;
font-size: 18px;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button[type="submit"]:hover {
background: #0056b3;
}
src/
├── App.jsx
├── main.jsx
├── WeatherApp.jsx
└── WeatherApp.css
import { useState } from 'react';
import './WeatherApp.css';
function WeatherApp() {
const [city, setCity] = useState('');
const [weather, setWeather] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const fetchWeather = async () => {
if (!city.trim()) {
setError('请输入城市名称');
return;
}
setLoading(true);
setError('');
try {
await new Promise(resolve => setTimeout(resolve, 1000));
const mockData = {
city: city,
temperature: Math.floor(Math.random() * 30) + 10,
condition: ['晴', '多云', '雨', '雪'][Math.floor(Math.random() * 4)],
humidity: Math.floor(Math.random() * 50) + 30,
wind: Math.floor(Math.random() * 20) + 5
};
setWeather(mockData);
} catch (err) {
setError('获取天气信息失败');
} finally {
setLoading(false);
}
};
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
fetchWeather();
}
};
return (
<div className="weather-app">
<h1>天气查询</h1>
<div className="search-box">
<input
value={city}
onChange={(e) => setCity(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="输入城市名称..."
/>
<button onClick={fetchWeather} disabled={loading}>
{loading ? '查询中...' : '查询'}
</button>
</div>
{error && <div className="error">{error}</div>}
{weather && !loading && (
<div className="weather-info">
<h2>{weather.city}</h2>
<div className="temperature">{weather.temperature}°C</div>
<div className="condition">{weather.condition}</div>
<div className="details">
<div>湿度: {weather.humidity}%</div>
<div>风速: {weather.wind} km/h</div>
</div>
</div>
)}
</div>
);
}
export default WeatherApp;
.weather-app {
max-width: 500px;
margin: 50px auto;
padding: 30px;
text-align: center;
}
.search-box {
display: flex;
gap: 10px;
margin: 30px 0;
}
.search-box input {
flex: 1;
padding: 12px;
font-size: 16px;
border: 2px solid #ddd;
border-radius: 5px;
}
.search-box button {
padding: 12px 24px;
font-size: 16px;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.search-box button:disabled {
background: #ccc;
cursor: not-allowed;
}
.error {
color: #dc3545;
padding: 10px;
margin: 20px 0;
background: #f8d7da;
border-radius: 5px;
}
.weather-info {
margin-top: 30px;
padding: 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 15px;
}
.temperature {
font-size: 72px;
font-weight: bold;
margin: 20px 0;
}
.condition {
font-size: 24px;
margin-bottom: 20px;
}
.details {
display: flex;
justify-content: space-around;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid rgba(255,255,255,0.3);
}
my-project/
├── public/
├── src/
│ ├── components/
│ │ ├── Counter.jsx
│ │ ├── Counter.css
│ │ ├── TodoApp.jsx
│ │ ├── TodoApp.css
│ │ ├── RegisterForm.jsx
│ │ ├── RegisterForm.css
│ │ ├── WeatherApp.jsx
│ │ └── WeatherApp.css
│ ├── App.jsx
│ ├── App.css
│ └── main.jsx
├── index.html
├── package.json
└── vite.config.js
import { useState } from 'react';
import Counter from './components/Counter';
import TodoApp from './components/TodoApp';
import RegisterForm from './components/RegisterForm';
import WeatherApp from './components/WeatherApp';
import './App.css';
function App() {
const [activeTab, setActiveTab] = useState('counter');
const tabs = [
{ id: 'counter', name: '计数器', component: <Counter /> },
{ id: 'todo', name: '待办事项', component: <TodoApp /> },
{ id: 'form', name: '表单验证', component: <RegisterForm /> },
{ id: 'weather', name: '天气查询', component: <WeatherApp /> }
];
return (
<div className="app">
<nav className="tabs">
{tabs.map(tab => (
<button
key={tab.id}
className={activeTab === tab.id ? 'active' : ''}
onClick={() => setActiveTab(tab.id)}
>
{tab.name}
</button>
))}
</nav>
<div className="content">
{tabs.find(tab => tab.id === activeTab)?.component}
</div>
</div>
);
}
export default App;
.app {
min-height: 100vh;
background: #f5f5f5;
}
.tabs {
display: flex;
gap: 10px;
padding: 20px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.tabs button {
padding: 10px 20px;
border: none;
background: #f0f0f0;
cursor: pointer;
border-radius: 5px;
font-size: 16px;
}
.tabs button.active {
background: #007bff;
color: white;
}
.content {
padding: 20px;
}
yarn create vite my-project --template react
cd my-project
yarn
mkdir src/components
yarn dev