Appearance
项目1:简易待办清单(Todo App)- 增删改查功能
1. 功能概述
在本章节中,我们将实现待办清单应用的核心功能:增删改查(CRUD)。这些功能包括:
- 创建(Create):添加新的待办事项
- 读取(Read):查看所有待办事项
- 更新(Update):修改待办事项的状态(完成/未完成)
- 删除(Delete):删除不需要的待办事项
我们已经在项目结构搭建中实现了基本的增删改查功能,现在将进一步完善和优化这些功能。
2. 增强添加待办事项功能
2.1 添加验证
为了提高用户体验,我们需要添加输入验证,确保用户不能添加空的待办事项:
javascript
// components/TodoInput.js
import React, { useState } from 'react';
import { View, TextInput, TouchableOpacity, Text, StyleSheet, Alert } from 'react-native';
const TodoInput = ({ onAddTodo }) => {
const [text, setText] = useState('');
const handleAdd = () => {
const trimmedText = text.trim();
if (trimmedText) {
onAddTodo(trimmedText);
setText('');
} else {
Alert.alert('提示', '请输入待办事项内容');
}
};
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="添加待办事项..."
value={text}
onChangeText={setText}
onSubmitEditing={handleAdd}
returnKeyType="done"
/>
<TouchableOpacity style={styles.button} onPress={handleAdd}>
<Text style={styles.buttonText}>添加</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
marginBottom: 20,
},
input: {
flex: 1,
borderWidth: 1,
borderColor: '#ddd',
padding: 10,
borderRadius: 5,
backgroundColor: '#fff',
marginRight: 10,
},
button: {
backgroundColor: '#007AFF',
padding: 10,
borderRadius: 5,
justifyContent: 'center',
alignItems: 'center',
},
buttonText: {
color: '#fff',
fontWeight: 'bold',
},
});
export default TodoInput;2.2 添加动画效果
为了让添加待办事项的过程更加流畅,我们可以添加动画效果:
javascript
// components/TodoInput.js
import React, { useState, useRef } from 'react';
import { View, TextInput, TouchableOpacity, Text, StyleSheet, Alert, Animated } from 'react-native';
const TodoInput = ({ onAddTodo }) => {
const [text, setText] = useState('');
const buttonScale = useRef(new Animated.Value(1)).current;
const handleAdd = () => {
const trimmedText = text.trim();
if (trimmedText) {
// 添加按钮动画
Animated.sequence([
Animated.timing(buttonScale, {
toValue: 0.9,
duration: 100,
useNativeDriver: true,
}),
Animated.timing(buttonScale, {
toValue: 1,
duration: 100,
useNativeDriver: true,
}),
]).start();
onAddTodo(trimmedText);
setText('');
} else {
Alert.alert('提示', '请输入待办事项内容');
}
};
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="添加待办事项..."
value={text}
onChangeText={setText}
onSubmitEditing={handleAdd}
returnKeyType="done"
/>
<Animated.View style={{ transform: [{ scale: buttonScale }] }}>
<TouchableOpacity style={styles.button} onPress={handleAdd}>
<Text style={styles.buttonText}>添加</Text>
</TouchableOpacity>
</Animated.View>
</View>
);
};
// 样式保持不变
export default TodoInput;3. 增强更新待办事项功能
3.1 添加动画效果
为了让更新待办事项状态的过程更加直观,我们可以添加动画效果:
javascript
// components/TodoItem.js
import React, { useRef } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Animated } from 'react-native';
const TodoItem = ({ todo, onPress, onDelete }) => {
const opacity = useRef(new Animated.Value(1)).current;
const scale = useRef(new Animated.Value(1)).current;
const handlePress = () => {
// 添加点击动画
Animated.sequence([
Animated.timing(scale, {
toValue: 0.95,
duration: 100,
useNativeDriver: true,
}),
Animated.timing(scale, {
toValue: 1,
duration: 100,
useNativeDriver: true,
}),
]).start();
onPress(todo.id);
};
return (
<Animated.View style={{ transform: [{ scale }], opacity }}>
<TouchableOpacity style={styles.container} onPress={handlePress}>
<View style={[styles.checkbox, todo.completed && styles.checkboxCompleted]}>
{todo.completed && <Text style={styles.checkmark}>✓</Text>}
</View>
<Text style={[styles.text, todo.completed && styles.textCompleted]}>
{todo.text}
</Text>
<TouchableOpacity onPress={() => onDelete(todo.id)} style={styles.deleteButton}>
<Text style={styles.deleteText}>×</Text>
</TouchableOpacity>
</TouchableOpacity>
</Animated.View>
);
};
// 样式保持不变
export default TodoItem;3.2 添加编辑功能
现在,我们将添加编辑待办事项的功能:
javascript
// components/TodoItem.js
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, TextInput, Alert } from 'react-native';
const TodoItem = ({ todo, onPress, onDelete, onEdit }) => {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(todo.text);
const handleEdit = () => {
const trimmedText = editText.trim();
if (trimmedText) {
onEdit(todo.id, trimmedText);
setIsEditing(false);
} else {
Alert.alert('提示', '待办事项内容不能为空');
}
};
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.checkboxContainer}
onPress={() => onPress(todo.id)}
>
<View style={[styles.checkbox, todo.completed && styles.checkboxCompleted]}>
{todo.completed && <Text style={styles.checkmark}>✓</Text>}
</View>
</TouchableOpacity>
{isEditing ? (
<TextInput
style={[styles.input, todo.completed && styles.textCompleted]}
value={editText}
onChangeText={setEditText}
onSubmitEditing={handleEdit}
returnKeyType="done"
autoFocus
/>
) : (
<TouchableOpacity
style={styles.textContainer}
onPress={() => setIsEditing(true)}
>
<Text style={[styles.text, todo.completed && styles.textCompleted]}>
{todo.text}
</Text>
</TouchableOpacity>
)}
<TouchableOpacity onPress={() => onDelete(todo.id)} style={styles.deleteButton}>
<Text style={styles.deleteText}>×</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
padding: 15,
borderRadius: 5,
marginBottom: 10,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 3.84,
elevation: 5,
},
checkboxContainer: {
marginRight: 10,
},
checkbox: {
width: 20,
height: 20,
borderWidth: 2,
borderColor: '#007AFF',
borderRadius: 4,
justifyContent: 'center',
alignItems: 'center',
},
checkboxCompleted: {
backgroundColor: '#007AFF',
},
checkmark: {
color: '#fff',
fontWeight: 'bold',
},
textContainer: {
flex: 1,
},
text: {
fontSize: 16,
},
textCompleted: {
textDecorationLine: 'line-through',
color: '#999',
},
input: {
flex: 1,
fontSize: 16,
padding: 5,
borderBottomWidth: 1,
borderBottomColor: '#007AFF',
},
deleteButton: {
padding: 5,
},
deleteText: {
fontSize: 24,
color: '#ff3b30',
fontWeight: 'bold',
},
});
export default TodoItem;4. 增强删除待办事项功能
4.1 添加确认对话框
为了防止用户误操作,我们可以添加删除确认对话框:
javascript
// screens/TodoListScreen.js
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, StyleSheet, Alert } from 'react-native';
import TodoItem from '../components/TodoItem';
import TodoInput from '../components/TodoInput';
import { storage } from '../utils/storage';
import { globalStyles } from '../styles/global';
const TodoListScreen = () => {
const [todos, setTodos] = useState([]);
// 加载待办事项
useEffect(() => {
loadTodos();
}, []);
const loadTodos = async () => {
const savedTodos = await storage.getTodos();
setTodos(savedTodos);
};
const saveTodos = async (newTodos) => {
setTodos(newTodos);
await storage.saveTodos(newTodos);
};
// 添加待办事项
const handleAddTodo = (text) => {
const newTodo = {
id: Date.now().toString(),
text,
completed: false,
};
saveTodos([...todos, newTodo]);
};
// 切换待办事项状态
const handleToggleTodo = (id) => {
const updatedTodos = todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
saveTodos(updatedTodos);
};
// 编辑待办事项
const handleEditTodo = (id, newText) => {
const updatedTodos = todos.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
);
saveTodos(updatedTodos);
};
// 删除待办事项
const handleDeleteTodo = (id) => {
Alert.alert(
'确认删除',
'确定要删除这个待办事项吗?',
[
{
text: '取消',
style: 'cancel',
},
{
text: '删除',
style: 'destructive',
onPress: () => {
const updatedTodos = todos.filter(todo => todo.id !== id);
saveTodos(updatedTodos);
},
},
],
{ cancelable: true }
);
};
// 渲染待办项
const renderItem = ({ item }) => (
<TodoItem
todo={item}
onPress={handleToggleTodo}
onDelete={handleDeleteTodo}
onEdit={handleEditTodo}
/>
);
return (
<View style={globalStyles.container}>
<Text style={globalStyles.title}>待办清单</Text>
<TodoInput onAddTodo={handleAddTodo} />
<FlatList
data={todos}
renderItem={renderItem}
keyExtractor={item => item.id}
style={globalStyles.list}
ListEmptyComponent={
<Text style={globalStyles.emptyText}>暂无待办事项</Text>
}
/>
</View>
);
};
export default TodoListScreen;4.2 添加删除动画
为了让删除操作更加直观,我们可以添加删除动画:
javascript
// components/TodoItem.js
import React, { useState, useRef } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, TextInput, Alert, Animated } from 'react-native';
const TodoItem = ({ todo, onPress, onDelete, onEdit }) => {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(todo.text);
const [isDeleting, setIsDeleting] = useState(false);
const slideAnim = useRef(new Animated.Value(0)).current;
const opacityAnim = useRef(new Animated.Value(1)).current;
const handleEdit = () => {
const trimmedText = editText.trim();
if (trimmedText) {
onEdit(todo.id, trimmedText);
setIsEditing(false);
} else {
Alert.alert('提示', '待办事项内容不能为空');
}
};
const handleDelete = () => {
Alert.alert(
'确认删除',
'确定要删除这个待办事项吗?',
[
{
text: '取消',
style: 'cancel',
},
{
text: '删除',
style: 'destructive',
onPress: () => {
setIsDeleting(true);
// 添加删除动画
Animated.parallel([
Animated.timing(slideAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}),
]).start(() => {
onDelete(todo.id);
});
},
},
],
{ cancelable: true }
);
};
return (
<Animated.View
style={[
styles.container,
{
transform: [{
translateX: slideAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, 300],
}),
}],
opacity: opacityAnim,
},
]}
>
<TouchableOpacity
style={styles.checkboxContainer}
onPress={() => onPress(todo.id)}
>
<View style={[styles.checkbox, todo.completed && styles.checkboxCompleted]}>
{todo.completed && <Text style={styles.checkmark}>✓</Text>}
</View>
</TouchableOpacity>
{isEditing ? (
<TextInput
style={[styles.input, todo.completed && styles.textCompleted]}
value={editText}
onChangeText={setEditText}
onSubmitEditing={handleEdit}
returnKeyType="done"
autoFocus
/>
) : (
<TouchableOpacity
style={styles.textContainer}
onPress={() => setIsEditing(true)}
>
<Text style={[styles.text, todo.completed && styles.textCompleted]}>
{todo.text}
</Text>
</TouchableOpacity>
)}
<TouchableOpacity onPress={handleDelete} style={styles.deleteButton}>
<Text style={styles.deleteText}>×</Text>
</TouchableOpacity>
</Animated.View>
);
};
// 样式保持不变
export default TodoItem;5. 增强读取功能
5.1 添加过滤功能
为了方便用户查看不同状态的待办事项,我们可以添加过滤功能:
javascript
// screens/TodoListScreen.js
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, StyleSheet, Alert, TouchableOpacity } from 'react-native';
import TodoItem from '../components/TodoItem';
import TodoInput from '../components/TodoInput';
import { storage } from '../utils/storage';
import { globalStyles } from '../styles/global';
const TodoListScreen = () => {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all'); // 'all', 'active', 'completed'
// 加载待办事项
useEffect(() => {
loadTodos();
}, []);
const loadTodos = async () => {
const savedTodos = await storage.getTodos();
setTodos(savedTodos);
};
const saveTodos = async (newTodos) => {
setTodos(newTodos);
await storage.saveTodos(newTodos);
};
// 添加待办事项
const handleAddTodo = (text) => {
const newTodo = {
id: Date.now().toString(),
text,
completed: false,
};
saveTodos([...todos, newTodo]);
};
// 切换待办事项状态
const handleToggleTodo = (id) => {
const updatedTodos = todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
saveTodos(updatedTodos);
};
// 编辑待办事项
const handleEditTodo = (id, newText) => {
const updatedTodos = todos.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
);
saveTodos(updatedTodos);
};
// 删除待办事项
const handleDeleteTodo = (id) => {
Alert.alert(
'确认删除',
'确定要删除这个待办事项吗?',
[
{
text: '取消',
style: 'cancel',
},
{
text: '删除',
style: 'destructive',
onPress: () => {
const updatedTodos = todos.filter(todo => todo.id !== id);
saveTodos(updatedTodos);
},
},
],
{ cancelable: true }
);
};
// 过滤待办事项
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
// 渲染待办项
const renderItem = ({ item }) => (
<TodoItem
todo={item}
onPress={handleToggleTodo}
onDelete={handleDeleteTodo}
onEdit={handleEditTodo}
/>
);
return (
<View style={globalStyles.container}>
<Text style={globalStyles.title}>待办清单</Text>
<TodoInput onAddTodo={handleAddTodo} />
{/* 过滤按钮 */}
<View style={styles.filterContainer}>
<TouchableOpacity
style={[styles.filterButton, filter === 'all' && styles.filterButtonActive]}
onPress={() => setFilter('all')}
>
<Text style={[styles.filterText, filter === 'all' && styles.filterTextActive]}>全部</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.filterButton, filter === 'active' && styles.filterButtonActive]}
onPress={() => setFilter('active')}
>
<Text style={[styles.filterText, filter === 'active' && styles.filterTextActive]}>未完成</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.filterButton, filter === 'completed' && styles.filterButtonActive]}
onPress={() => setFilter('completed')}
>
<Text style={[styles.filterText, filter === 'completed' && styles.filterTextActive]}>已完成</Text>
</TouchableOpacity>
</View>
<FlatList
data={filteredTodos}
renderItem={renderItem}
keyExtractor={item => item.id}
style={globalStyles.list}
ListEmptyComponent={
<Text style={globalStyles.emptyText}>
{filter === 'all' ? '暂无待办事项' :
filter === 'active' ? '暂无未完成的待办事项' : '暂无已完成的待办事项'}
</Text>
}
/>
</View>
);
};
const styles = StyleSheet.create({
filterContainer: {
flexDirection: 'row',
marginBottom: 15,
justifyContent: 'space-between',
},
filterButton: {
flex: 1,
padding: 10,
borderRadius: 5,
backgroundColor: '#e0e0e0',
marginHorizontal: 5,
alignItems: 'center',
},
filterButtonActive: {
backgroundColor: '#007AFF',
},
filterText: {
color: '#333',
},
filterTextActive: {
color: '#fff',
fontWeight: 'bold',
},
});
export default TodoListScreen;5.2 添加统计功能
为了让用户了解待办事项的整体情况,我们可以添加统计功能:
javascript
// screens/TodoListScreen.js
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, StyleSheet, Alert, TouchableOpacity } from 'react-native';
import TodoItem from '../components/TodoItem';
import TodoInput from '../components/TodoInput';
import { storage } from '../utils/storage';
import { globalStyles } from '../styles/global';
const TodoListScreen = () => {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all'); // 'all', 'active', 'completed'
// 加载待办事项
useEffect(() => {
loadTodos();
}, []);
const loadTodos = async () => {
const savedTodos = await storage.getTodos();
setTodos(savedTodos);
};
const saveTodos = async (newTodos) => {
setTodos(newTodos);
await storage.saveTodos(newTodos);
};
// 添加待办事项
const handleAddTodo = (text) => {
const newTodo = {
id: Date.now().toString(),
text,
completed: false,
};
saveTodos([...todos, newTodo]);
};
// 切换待办事项状态
const handleToggleTodo = (id) => {
const updatedTodos = todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
saveTodos(updatedTodos);
};
// 编辑待办事项
const handleEditTodo = (id, newText) => {
const updatedTodos = todos.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
);
saveTodos(updatedTodos);
};
// 删除待办事项
const handleDeleteTodo = (id) => {
Alert.alert(
'确认删除',
'确定要删除这个待办事项吗?',
[
{
text: '取消',
style: 'cancel',
},
{
text: '删除',
style: 'destructive',
onPress: () => {
const updatedTodos = todos.filter(todo => todo.id !== id);
saveTodos(updatedTodos);
},
},
],
{ cancelable: true }
);
};
// 清除已完成的待办事项
const handleClearCompleted = () => {
if (todos.some(todo => todo.completed)) {
Alert.alert(
'确认清除',
'确定要清除所有已完成的待办事项吗?',
[
{
text: '取消',
style: 'cancel',
},
{
text: '清除',
style: 'destructive',
onPress: () => {
const updatedTodos = todos.filter(todo => !todo.completed);
saveTodos(updatedTodos);
},
},
],
{ cancelable: true }
);
}
};
// 过滤待办事项
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
// 统计信息
const totalTodos = todos.length;
const completedTodos = todos.filter(todo => todo.completed).length;
const activeTodos = totalTodos - completedTodos;
// 渲染待办项
const renderItem = ({ item }) => (
<TodoItem
todo={item}
onPress={handleToggleTodo}
onDelete={handleDeleteTodo}
onEdit={handleEditTodo}
/>
);
return (
<View style={globalStyles.container}>
<Text style={globalStyles.title}>待办清单</Text>
<TodoInput onAddTodo={handleAddTodo} />
{/* 统计信息 */}
<View style={styles.statsContainer}>
<Text style={styles.statsText}>
共 {totalTodos} 项,{activeTodos} 项未完成,{completedTodos} 项已完成
</Text>
{completedTodos > 0 && (
<TouchableOpacity onPress={handleClearCompleted}>
<Text style={styles.clearText}>清除已完成</Text>
</TouchableOpacity>
)}
</View>
{/* 过滤按钮 */}
<View style={styles.filterContainer}>
<TouchableOpacity
style={[styles.filterButton, filter === 'all' && styles.filterButtonActive]}
onPress={() => setFilter('all')}
>
<Text style={[styles.filterText, filter === 'all' && styles.filterTextActive]}>全部</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.filterButton, filter === 'active' && styles.filterButtonActive]}
onPress={() => setFilter('active')}
>
<Text style={[styles.filterText, filter === 'active' && styles.filterTextActive]}>未完成</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.filterButton, filter === 'completed' && styles.filterButtonActive]}
onPress={() => setFilter('completed')}
>
<Text style={[styles.filterText, filter === 'completed' && styles.filterTextActive]}>已完成</Text>
</TouchableOpacity>
</View>
<FlatList
data={filteredTodos}
renderItem={renderItem}
keyExtractor={item => item.id}
style={globalStyles.list}
ListEmptyComponent={
<Text style={globalStyles.emptyText}>
{filter === 'all' ? '暂无待办事项' :
filter === 'active' ? '暂无未完成的待办事项' : '暂无已完成的待办事项'}
</Text>
}
/>
</View>
);
};
const styles = StyleSheet.create({
statsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 15,
},
statsText: {
fontSize: 14,
color: '#666',
},
clearText: {
fontSize: 14,
color: '#007AFF',
},
filterContainer: {
flexDirection: 'row',
marginBottom: 15,
justifyContent: 'space-between',
},
filterButton: {
flex: 1,
padding: 10,
borderRadius: 5,
backgroundColor: '#e0e0e0',
marginHorizontal: 5,
alignItems: 'center',
},
filterButtonActive: {
backgroundColor: '#007AFF',
},
filterText: {
color: '#333',
},
filterTextActive: {
color: '#fff',
fontWeight: 'bold',
},
});
export default TodoListScreen;6. 完整的增删改查功能
现在,我们已经实现了完整的增删改查功能,包括:
- 添加待办事项:支持添加新的待办事项,包含输入验证和动画效果
- 查看待办事项:支持查看所有待办事项,可按状态过滤
- 更新待办事项:支持切换待办事项的完成状态和编辑待办事项内容
- 删除待办事项:支持删除单个待办事项和清除所有已完成的待办事项,包含确认对话框和删除动画
- 统计功能:显示待办事项的总数、未完成数和已完成数
7. 测试功能
执行以下命令运行项目,测试增删改查功能:
bash
# 启动开发服务器
npx expo start
# 在 iOS 模拟器中运行
# 按 i
# 在 Android 模拟器中运行
# 按 a
# 在网页中运行
# 按 w8. 总结
在本章节中,我们实现了待办清单应用的核心功能:增删改查。通过添加验证、动画效果、过滤功能和统计功能,我们提高了应用的用户体验。这些功能的实现不仅展示了 React Native 的基本用法,也为我们后续的开发打下了基础。
在接下来的章节中,我们将进一步完善本地数据存储和美化应用的用户界面。
