NativeStackNavigationProp
:类型安全的导航属性navigation.reset
:用于登录后重置导航栈multiSet
原子操作存储多个键值对useState
:管理组件本地状态(表单输入、加载状态)useLayoutEffect
:处理界面布局相关副作用采用集中式API配置(API_CONFIG
):
API_CONFIG.getUrl(API_CONFIG.ENDPOINTS.LOGIN) // 登录接口
API_CONFIG.getUrl(API_CONFIG.ENDPOINTS.REGISTER) // 注册接口
API_CONFIG.getUrl(API_CONFIG.ENDPOINTS.CHANGE_PASSWORD(userId)) // 修改密码接口
特性 | 登录 | 注册 | 修改密码 |
---|---|---|---|
方法 | POST | POST | PUT |
Headers | Content-Type: JSON | Content-Type: JSON | Content-Type: JSON + Authorization |
Body | {phone, password} | {phone, password, name} | {oldPassword, newPassword} |
成功响应:
{"message": "登录成功"}
错误响应:
视觉元素:
ImageBackground
:带背景图的登录界面TextInput
:手机号(数字键盘)和密码(安全输入)TouchableOpacity
:登录按钮带加载指示器状态管理:
const [phone, setPhone] = useState(''); // 手机号
const [password, setPassword] = useState(''); // 密码
const [loading, setLoading] = useState(false); // 加载状态
核心流程:
authService.login
export const login = async (phone: string, password: string) => {
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, password }),
});
// 错误处理
if (!response.ok) {
const errorData = await response.json();
if (response.status === 401) throw new Error('用户不存在');
if (response.status === 500) throw new Error('服务器错误');
throw new Error(errorData.message || '登录失败');
}
// 成功处理
const token = response.headers.get('Authorization')?.split(' ')[1] || '';
const userId = response.headers.get('X-User-ID') || '';
return { message: data.message, token, userId };
}
export const register = async (phone: string, password: string, name: string) => {
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, password, name }),
});
if (response.status === 409) throw new Error('用户已存在');
if (response.status === 500) throw new Error('服务器错误');
if (!response.ok) throw new Error('注册失败');
return await response.text(); // 返回"注册成功"等文本
}
export const changePassword = async (userId: string, oldPassword: string, newPassword: string) => {
const token = await AsyncStorage.getItem('token');
if (!token) throw new Error('用户未登录');
const response = await fetch(API_URL, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ oldPassword, newPassword }),
});
if (response.status === 404) throw new Error('用户不存在');
if (response.status === 400) throw new Error('原密码错误');
if (response.status === 500) throw new Error('服务器错误');
return await response.text() || '密码修改成功';
}
AsyncStorage
安全存储secureTextEntry
keyboardType="phone-pad"
ActivityIndicator
Alert.alert
模态框// 登录成功后清除导航栈
navigation.reset({ index: 0, routes: [{ name: 'MainTabs' }] });
// 注册成功后建议跳转逻辑
navigation.navigate('Login', { registeredPhone: phone });
textShadow
系列属性)shadow
系列属性)TouchableOpacity
)// 模拟成功响应
const mockSuccessResponse = {
json: async () => ({ message: "登录成功" }),
ok: true,
headers: {
get: (header: string) =>
header === "Authorization" ? "Bearer fake-token" : "123"
}
}
// 模拟错误响应
const mockErrorResponse = {
json: async () => ({ message: "无效凭证" }),
ok: false,
status: 401
}
登录测试:
注册测试:
// 提取所有字符串为常量
const STRINGS = {
LOGIN_FAILED: '登录失败',
REGISTER_SUCCESS: '注册成功',
// ...
}
const themes = {
light: {
inputBg: '#ffffff',
textColor: '#222222'
},
dark: {
inputBg: '#333333',
textColor: '#ffffff'
}
}
// 统一API错误处理拦截器
const apiClient = async (endpoint: string, config: RequestInit) => {
try {
const response = await fetch(endpoint, config);
if (!response.ok) throw new ApiError(response.status);
return await response.json();
} catch (error) {
// 统一错误处理逻辑
}
}
实现了React Native平台下完整的用户登陆注册认证流程:
数据流:用户输入 → 前端验证 → API请求 → 响应处理 → 状态更新 → 界面反馈 → 导航跳转
用户资料管理系统
├── 资料展示模块 (UserProfileScreen)
├── 资料编辑模块 (ProfileForm)
├── 密码修改模块 (ChangePasswordScreen)
├── 账号安全模块
│ ├── 退出登录功能
│ └── 账号注销功能
└── 服务层 (profileService, authService)
// 获取用户资料
const loadProfile = async () => {
try {
const userId = await AsyncStorage.getItem('userId');
const data = await getUserProfile(userId);
setProfile({
name: data.name,
gender: data.gender,
birthYear: data.birthYear,
occupation: data.occupation,
height: data.height,
healthDescription: data.healthDescription
});
} catch (err) {
Alert.alert('加载失败', err.message);
}
};
// 资料卡片组件
const InfoRow = ({ label, value }) => (
<View style={styles.infoRow}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{value || '--'}</Text>
</View>
);
const handleSubmit = async () => {
// 前端验证
const validationMsg = validatePassword(newPassword);
if (validationMsg) return Alert.alert('密码不符合要求', validationMsg);
if (newPassword !== confirmPassword) return Alert.alert('两次密码不一致');
try {
setIsLoading(true);
const userId = await AsyncStorage.getItem('userId');
await changePassword(userId, currentPassword, newPassword);
Alert.alert('成功', '密码修改成功,请重新登录', [{
text: '确定',
onPress: () => navigation.reset({ index: 0, routes: [{ name: 'Login' }] })
}]);
} catch (err) {
Alert.alert('修改失败', err.message);
} finally {
setIsLoading(false);
}
};
const handleLogout = () => {
Alert.alert('确认退出', '确定要退出登录吗?', [
{ text: '取消', style: 'cancel' },
{
text: '退出',
onPress: () => {
navigation.reset({
index: 0,
routes: [{ name: 'Login' }]
});
}
},
]);
};
const handleDelete = async () => {
Alert.alert('确认注销', '账号信息将会永久删除', [
{ text: '取消', style: 'cancel' },
{
text: '确认删除',
style: 'destructive',
onPress: async () => {
await deleteUser(userId);
await AsyncStorage.multiRemove(['token', 'userId']);
navigation.reset({ index: 0, routes: [{ name: 'Login' }] });
}
},
]);
};
export const getUserProfile = async (userId: string): Promise<UserProfile> => {
try {
const token = await AsyncStorage.getItem('token');
const response = await fetch(API_CONFIG.getUrl(API_CONFIG.ENDPOINTS.GET_PROFILE(userId)), {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) throw new Error('获取资料失败');
const data = await response.json();
await AsyncStorage.setItem(PROFILE_KEY, JSON.stringify(data)); // 缓存数据
return data;
} catch (error) {
const cached = await AsyncStorage.getItem(PROFILE_KEY); // 降级读取缓存
if (cached) return JSON.parse(cached);
throw error;
}
};
export const changePassword = async (userId: string, oldPassword: string, newPassword: string) => {
const token = await AsyncStorage.getItem('token');
const response = await fetch(API_CONFIG.getUrl(API_CONFIG.ENDPOINTS.CHANGE_PASSWORD(userId)), {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ oldPassword, newPassword })
});
if (response.status === 400) throw new Error('原密码错误');
if (response.status === 404) throw new Error('用户不存在');
if (!response.ok) throw new Error('修改密码失败');
return await response.text();
};
组件类别 | 样式特征 | 示例 |
---|---|---|
卡片 | 白色背景+像素边框 | borderWidth: 2 |
按钮 | 圆角+阴影+图标 | borderRadius: 20 |
输入框 | 浅黄背景+像素边框 | backgroundColor: #fef0d4 |
危险操作 | 红色警示色 | backgroundColor: #DD4B4B |
键盘适应:
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
{/* 表单内容 */}
</KeyboardAvoidingView>
加载状态:
<TouchableOpacity
disabled={isLoading}
onPress={handleSubmit}
>
{isLoading ? <ActivityIndicator /> : <Text>提交</Text>}
</TouchableOpacity>
表单验证反馈:
<TextInput
onChangeText={(text) => {
setNewPassword(text);
setErrorMsg(validatePassword(text)); // 实时验证
}}
/>
{errorMsg && <Text style={styles.error}>{errorMsg}</Text>}
措施 | 实现方式 |
---|---|
密码传输加密 | HTTPS + 请求体加密 |
Token 安全存储 | AsyncStorage + 自动刷新机制 |
敏感操作确认 | Alert二次确认 |
密码复杂度要求 | 前端实时验证+后端强制校验 |
const validatePassword = (pwd) => {
const rules = [
{ test: pwd.length >= 8, msg: '至少8个字符' },
{ test: /[A-Z]/.test(pwd), msg: '大写字母' },
{ test: /[a-z]/.test(pwd), msg: '小写字母' },
{ test: /[0-9]/.test(pwd), msg: '数字' }
];
const failed = rules.filter(r => !r.test);
return failed.length ? `需要包含: ${failed.map(r => r.msg).join(', ')}` : '';
};
// 优先网络请求,失败时降级读取缓存
try {
const freshData = await fetchFreshData();
await cacheData(freshData);
return freshData;
} catch (error) {
const cached = await getCachedData();
return cached || Promise.reject(error);
}
组件记忆化:
const InfoRow = React.memo(({ label, value }) => (
<View style={styles.infoRow}>
<Text>{label}</Text>
<Text>{value}</Text>
</View>
));
按需渲染:
const [visibleSections, setVisibleSections] = useState({
basic: true,
health: false
});
const themes = {
pixel: {
primaryColor: '#75A6FF',
borderStyle: { borderWidth: 2, borderColor: '#003366' }
},
modern: {
primaryColor: '#6200ee',
borderStyle: { borderWidth: 1, borderColor: '#e0e0e0' }
}
};
const i18n = {
en: {
profileTitle: 'Profile',
logoutConfirm: 'Are you sure to logout?'
},
zh: {
profileTitle: '个人资料',
logoutConfirm: '确定要退出登录吗?'
}
};
const t = (key) => i18n[currentLanguage][key];
用户资料管理功能实现了:
典型用户数据流:查看资料 → 编辑提交 → 密码修改 → 安全退出 → 重新登录
技术/库 | 用途 | 应用场景 |
---|---|---|
react-native-chart-kit | 数据可视化 | 绘制血糖、血压、体重趋势图表 |
Axios/Fetch | 网络请求 | 与后端API交互 |
AsyncStorage | 本地数据存储 | 用户认证信息持久化 |
身体指标模块
├── BodyMetricsScreen.tsx # 指标可视化主界面
│ ├── 趋势图表展示
│ ├── 历史数据列表
│ └── 导航到录入界面
├── BodyMetricsSubmitScreen.tsx # 指标录入/修改界面
│ ├── 表单输入
│ ├── 数据验证
│ └── 数据提交
└── metricService.ts # 数据服务层
├── 数据获取
├── 数据提交
└── 数据删除
// 获取用户历史指标数据
const loadMetrics = async () => {
try {
const userId = await AsyncStorage.getItem('userId');
const history = await getUserBodyMetrics(userId);
// 数据预处理
const processedData = history.map(item => ({
...item,
date: formatDate(item.date) // 统一日期格式
}));
setMetricsHistory(processedData);
} catch (error) {
Alert.alert('获取失败', error.message);
}
};
// 图表数据准备
const prepareChartData = (metricType: 'fbg'|'pbg'|'weight') => {
return {
labels: metricsHistory.map(item => item.date),
datasets: [{
data: metricsHistory.map(item => item[metricType]),
color: () => getColorByMetric(metricType), // 根据指标类型返回不同颜色
strokeWidth: 2
}]
};
};
<ScrollView>
{/* 空腹血糖图表 */}
<LineChart
data={prepareChartData('fbg')}
width={screenWidth - 32}
height={220}
yAxisSuffix=" mmol/L"
chartConfig={chartConfig}
style={styles.chart}
/>
{/* 血压双线图表 */}
<LineChart
data={{
labels: metricsHistory.map(item => item.date),
datasets: [
{ data: metricsHistory.map(item => item.sbp), ...sbpStyle },
{ data: metricsHistory.map(item => item.dbp), ...dbpStyle }
]
}}
width={screenWidth - 32}
height={220}
yAxisSuffix=" mmHg"
/>
</ScrollView>
// 表单状态管理
const [formData, setFormData] = useState<BodyMetrics>({
fbg: '',
pbg: '',
sbp: '',
dbp: '',
weight: ''
});
// 表单验证
const validateForm = () => {
const rules = [
{ field: 'fbg', validator: v => v === '' || (v >= 2 && v <= 20), message: '空腹血糖值异常' },
{ field: 'sbp', validator: v => v === '' || (v >= 60 && v <= 250), message: '收缩压异常' }
];
return rules.every(rule => {
const isValid = rule.validator(formData[rule.field]);
if (!isValid) Alert.alert('输入错误', rule.message);
return isValid;
});
};
// 输入处理
const handleInputChange = (field: keyof BodyMetrics, value: string) => {
setFormData(prev => ({
...prev,
[field]: value.replace(/[^0-9.]/g, '') // 只允许数字和小数点
}));
};
const handleSubmit = async () => {
if (!validateForm()) return;
try {
const payload = {
userId: await AsyncStorage.getItem('userId'),
date: new Date().toISOString().split('T')[0],
...Object.fromEntries(
Object.entries(formData).map(([k,v]) => [k, v === '' ? undefined : parseFloat(v)])
};
await saveBodyMetrics(payload);
Alert.alert('成功', '数据已保存', [{
text: '确定',
onPress: () => navigation.goBack()
}]);
} catch (error) {
Alert.alert('提交失败', error.message);
}
};
// 获取用户历史数据
export const getUserBodyMetrics = async (userId: string): Promise<BodyMetrics[]> => {
const response = await api.get<BodyMetrics[]>(
API_CONFIG.getUrl(API_CONFIG.ENDPOINTS.BODY_METRIC_GET_USER(userId))
);
return response.data;
};
// 提交指标数据
export const saveBodyMetrics = async (data: BodyMetrics): Promise<void> => {
await api.post(
API_CONFIG.getUrl(API_CONFIG.ENDPOINTS.BODY_METRIC_SAVE),
data
);
};
// 数据类型定义
export interface BodyMetrics {
entryId?: number;
userId: string;
date: string;
fbg?: number; // 空腹血糖(mmol/L)
pbg?: number; // 餐后血糖(mmol/L)
sbp?: number; // 收缩压(mmHg)
dbp?: number; // 舒张压(mmHg)
weight?: number; // 体重(kg)
}
const screenWidth = Dimensions.get('window').width;
<LineChart width={screenWidth - 32} ... />
<LineChart
onDataPointClick={({value, index}) =>
Alert.alert(`详细数据`, `${metricsHistory[index].date}: ${value}`)
}
/>
<TextInput
keyboardType="decimal-pad"
placeholder="空腹血糖(mmol/L)"
value={formData.fbg}
onChangeText={v => handleInputChange('fbg', v)}
/>
<TouchableOpacity
style={[styles.button, !hasChanges && styles.disabledButton]}
disabled={!hasChanges}
onPress={handleSubmit}
>
<Text>保存数据</Text>
</TouchableOpacity>
const safeInput = input.replace(/[^0-9.]/g, '');
const [cachedData, setCachedData] = useState<BodyMetrics[]>([]);
// 获取数据时优先检查缓存
if (cachedData.length > 0) return cachedData;
const freshData = await fetchData();
setCachedData(freshData);
<FlatList
data={metricsHistory}
keyExtractor={item => item.date}
renderItem={({item}) => <MetricItem data={item} />}
/>
const [selectedDate, setSelectedDate] = useState(new Date());
<DatePicker
current={selectedDate}
onDateChange={setSelectedDate}
/>
const exportToCSV = () => {
const csv = metricsHistory.map(item =>
`${item.date},${item.fbg},${item.weight}`
).join('\n');
shareFile(csv, 'health_metrics.csv');
};
try {
await saveBodyMetrics(data);
} catch (error) {
if (error.response?.status === 401) {
navigation.navigate('Login');
} else {
logError(error);
showToast('保存失败,请重试');
}
}
const logMetricUpdate = (action: string, data: BodyMetrics) => {
console.log(`[${new Date().toISOString()}] ${action}:`, {
userId: data.userId,
date: data.date,
fields: Object.keys(data).filter(k => data[k] !== undefined)
});
};
实现了完整的身体指标管理功能:
可视化:
数据管理:
扩展性:
数据流:用户录入指标 → 系统验证数据 → 提交到服务端 → 可视化展示历史趋势 → 发现健康变化规律
技术/库 | 用途 | 应用场景 |
---|---|---|
ImageBackground | 背景设计 | 页面整体视觉风格 |
// 胶囊按钮样式
const capsuleStyle = {
borderRadius: 25, // 高圆角形成胶囊形状
paddingHorizontal: 20,
paddingVertical: 10,
backgroundColor: '#428DCB',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
elevation: 3
}
// 应用示例
<TouchableOpacity style={capsuleStyle}>
<Text style={{color: 'white'}}>提交反馈</Text>
</TouchableOpacity>
// 图标配置方案
const settingIcons = {
theme: 'palette',
notification: 'bell',
language: 'language',
feedback: 'message-square'
}
// 使用示例
<Icon
name={settingIcons.theme}
size={24}
color={isDarkTheme ? '#f5dd4b' : '#333'}
/>
交互类型 | 实现方式 | 视觉反馈 |
---|---|---|
按钮点击 | TouchableOpacity | 按压透明度变化 |
开关切换 | Switch组件 | 滑块动画+颜色变化 |
表单提交 | ActivityIndicator | 加载旋转图标 |
// 设置页面状态管理
const [settings, setSettings] = useState({
theme: 'light',
notifications: true,
language: 'zh-CN'
});
// 持久化存储
useEffect(() => {
const loadSettings = async () => {
const saved = await AsyncStorage.getItem('userSettings');
if (saved) setSettings(JSON.parse(saved));
};
loadSettings();
}, []);
const updateSetting = (key, value) => {
const newSettings = {...settings, [key]: value};
setSettings(newSettings);
AsyncStorage.setItem('userSettings', JSON.stringify(newSettings));
};
const FeedbackScreen = () => {
const [feedback, setFeedback] = useState('');
const [status, setStatus] = useState<'idle'|'submitting'|'success'|'error'>('idle');
const handleSubmit = async () => {
if (!feedback.trim()) {
Alert.alert('提示', '请输入有效反馈内容');
return;
}
setStatus('submitting');
try {
// 模拟网络请求
await new Promise(resolve => setTimeout(resolve, 1500));
setStatus('success');
setFeedback('');
} catch {
setStatus('error');
}
};
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="您的宝贵意见..."
value={feedback}
onChangeText={setFeedback}
multiline
/>
<TouchableOpacity
style={[
styles.submitButton,
status === 'submitting' && styles.disabledButton
]}
onPress={handleSubmit}
disabled={status === 'submitting'}
>
{status === 'submitting' ? (
<ActivityIndicator color="white" />
) : (
<Text style={styles.buttonText}>提交反馈</Text>
)}
</TouchableOpacity>
{status === 'success' && (
<Text style={styles.successText}>感谢您的反馈!</Text>
)}
</View>
);
};
// 主题上下文
const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {}
});
// 主题切换实现
const ThemeProvider = ({children}) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
AsyncStorage.setItem('appTheme', newTheme);
};
return (
<ThemeContext.Provider value={{theme, toggleTheme}}>
{children}
</ThemeContext.Provider>
);
};
元素 | 交互效果 | 实现方式 |
---|---|---|
按钮 | 按压缩小动画 | Animated API |
输入框 | 聚焦边框高亮 | onFocus/onBlur事件 |
开关控件 | 平滑滑动过渡 | Switch组件的trackColor |
// 响应式尺寸计算
const {width, height} = Dimensions.get('window');
const responsiveSize = (size: number) => {
const guidelineWidth = 375; // 设计稿基准宽度
return (size * width) / guidelineWidth;
};
// 应用示例
const styles = StyleSheet.create({
container: {
padding: responsiveSize(20),
},
input: {
height: responsiveSize(150),
}
});
// 屏幕方向监听
const [orientation, setOrientation] = useState(
Dimensions.get('window').width > Dimensions.get('window').height
? 'landscape'
: 'portrait'
);
useEffect(() => {
const subscription = Dimensions.addEventListener('change', ({window}) => {
setOrientation(window.width > window.height ? 'landscape' : 'portrait');
});
return () => subscription?.remove();
}, []);
数据流:用户打开设置页 → 加载本地偏好 → 修改设置 → 实时保存 → 视觉反馈;用户提交反馈 → 前端验证 → 模拟请求 → 结果提示。
首页首先需要展示用户的基本信息(如用户名、年龄、身高、身体描述等)。我们通过调用后端接口 getUserProfile
来获取这些数据。
步骤:
AsyncStorage.getItem
获取存储在本地的用户 ID(userId
)。getUserProfile
服务函数,传递 userId
来从后端获取用户资料。useState
钩子更新页面中的状态(例如:setUsername
、setBirthYear
、setHeight
、setHealthDesc
)。相关代码:
// 从AsyncStorage获取用户ID
const userId = await AsyncStorage.getItem('userId');
if (!userId) {
Alert.alert('错误', '无法获取用户ID,请重新登录');
return;
}
// 调用后端接口获取用户资料
const profile = await getUserProfile(userId);
if (profile) {
setUsername(profile.name || '');
setBirthYear(profile.birthYear || null);
setHeight(profile.height || null);
setHealthDesc(profile.healthDescription || '');
}
我们需要获取用户当天的锻炼和用药计划。通过调用 getTodayExercisePlans
和 getTodayMedicinePlans
函数从后端获取当天的计划。
步骤:
useFocusEffect
中,确保每次页面获取焦点时都会调用后端 API 来获取当天的计划。setTodayExercise
和 setTodayMedicine
分别将锻炼和用药计划保存到组件的状态中。相关代码:
const ex = await getTodayExercisePlans(userId); // 获取锻炼计划
const med = await getTodayMedicinePlans(userId); // 获取用药计划
setTodayExercise(ex); // 更新锻炼计划
setTodayMedicine(med); // 更新用药计划
提供删除按钮,用户可以删除当天已完成的锻炼或用药计划。点击删除按钮后,调用相应的删除函数 (deleteExercisePlan
或 deleteMedicinePlan
) 来删除计划,并在删除后刷新页面,确保显示的数据是最新的。
步骤:
handleDelete
函数。type
(exercise
或 medicine
)调用对应的删除 API。setTodayExercise
或 setTodayMedicine
来更新页面上的计划。相关代码:
const handleDelete = async (id: string, type: 'exercise' | 'medicine') => {
try {
if (type === 'exercise') {
await deleteExercisePlan(id);
setTodayExercise(prev => prev.filter(item => item.id !== id)); // 删除本地锻炼计划
} else {
await deleteMedicinePlan(id);
setTodayMedicine(prev => prev.filter(item => item.id !== id)); // 删除本地用药计划
}
} catch (error) {
Alert.alert('错误', '删除计划失败');
}
};
用户可以在页面中输入锻炼计划的描述,点击提交按钮后,锻炼计划会被发送到后端进行生成,并保存在后端数据库中。
步骤:
generateExercisePlans
函数将描述提交到后端,后端根据描述生成新的锻炼计划。fetchAllPlans
刷新页面上的计划列表,显示最新的锻炼计划。相关代码:
const handleSubmitExercise = async () => {
if (!userId) return Alert.alert('错误', '请先登录');
if (!exerciseDescription) return Alert.alert('提示', '请输入描述');
setExerciseLoading(true);
try {
// 调用后端生成锻炼计划
await generateExercisePlans(userId, exerciseDescription);
await fetchAllPlans(); // 刷新锻炼计划列表
setExerciseDescription(''); // 清空输入框
Alert.alert('成功', '锻炼计划已生成');
} catch (error) {
Alert.alert('生成失败', error.message);
} finally {
setExerciseLoading(false);
}
};
用户也可以在输入框中填写用药计划,提交后,后端会根据描述生成用药计划,并将其保存到数据库中。
步骤:
generateMedicinePlans
函数将描述发送到后端,生成用药计划。相关代码:
const handleSubmitMedicine = async () => {
if (!userId) return Alert.alert('错误', '请先登录');
if (!medicineDescription) return Alert.alert('提示', '请输入用药信息');
setMedicineLoading(true);
try {
// 调用后端生成用药计划
await generateMedicinePlans(userId, medicineDescription);
await fetchAllPlans(); // 刷新用药计划列表
setMedicineDescription(''); // 清空输入框
Alert.alert('成功', '用药计划已生成');
} catch (error) {
Alert.alert('生成失败', error.message);
} finally {
setMedicineLoading(false);
}
};
在提交计划(锻炼计划或用药计划)后,使用 fetchAllPlans
函数来刷新页面上的计划列表,确保页面显示的是最新的计划数据。
步骤:
fetchAllPlans
从后端获取最新的锻炼和用药计划。setExercisePlans
和 setMedicinePlans
更新页面上的状态,重新渲染列表。相关代码:
const fetchAllPlans = async () => {
try {
setRefreshing(true);
const [exercises, medications] = await Promise.all([
getExercisePlans(userId),
getMedicinePlans(userId)
]);
setExercisePlans(exercises); // 更新锻炼计划
setMedicinePlans(medications); // 更新用药计划
} catch (error) {
Alert.alert('获取计划失败', error.message);
} finally {
setRefreshing(false);
}
};
用户可以选择食物图片进行上传,上传后会进行分析并返回健康建议。
步骤:
launchImageLibrary
打开图库选择图片。通过设置合适的 options
(例如:最大宽度、最大高度、图片质量等)来控制上传图片的质量。setImageAsset
来保存选中的图片。相关代码:
const pickImage = () => {
const options = {
mediaType: 'photo',
maxWidth: 1024,
maxHeight: 1024,
quality: 0.8,
};
launchImageLibrary(options, (res) => {
if (res.didCancel) {
console.log('用户取消了图片选择');
} else if (res.errorCode) {
console.log('图片选择错误:', res.errorMessage);
} else if (res.assets && res.assets.length > 0) {
const asset = res.assets[0];
setImageAsset({
uri: asset.uri,
type: asset.type || 'image/jpeg',
name: asset.fileName || 'image.jpg',
});
}
});
};
上传食物图片后,调用后端接口 analyzeFoodImage
来分析图片内容,并返回健康建议。
步骤:
analyzeFoodImage
函数并传递图片文件,函数返回食物分析结果。setResponse
将分析结果保存到状态中,并在页面中渲染建议内容。相关代码:
const handleSubmit = async () => {
if (!imageAsset) {
Alert.alert('提示', '请上传食物图片');
return;
}
try {
setLoading(true);
const result = await analyzeFoodImage(imageAsset); // 调用后端接口分析图片
setResponse(result); // 更新健康建议
} catch (err) {
Alert.alert('错误', '食物分析失败,请稍后重试');
} finally {
setLoading(false);
}
};
将从后端获取到的健康建议渲染到页面上,使用 Markdown
组件来格式化显示结果,提供良好的用户体验。
步骤:
Markdown
组件来渲染返回的健康建议文本。Markdown
组件的样式,使得展示结果更加友好。相关代码:
{response && (
<View style={styles.responseBox}>
<Text style={styles.responseTitle}>健康分析与建议</Text>
<Markdown
style={{
body: styles.markdownBody,
heading1: styles.markdownH1,
heading2: styles.markdownH2,
heading3: styles.markdownH3,
_item: styles.markdownListItem,
text: styles.markdownText,
strong: styles.markdownStrong,
}}
>
{response}
</Markdown>
</View>
)}
用户选择角色后,通过角色提示,发送消息给AI并显示AI的回答。
步骤:
相关代码:
const handleRoleSelect = (role: string) => {
setSelectedRole(role); // 设置选中的角色
setShowOptions(false); // 关闭角色选择菜单
};
const handleSend = async () => {
if (!userInput.trim()) return;
const rolePrompt = selectedRole ? `请以${selectedRole}的身份回答:` : '';
const newMessages = [...messages, { role: 'user', text: rolePrompt + userInput }];
setMessages(newMessages);
try {
const aiResponse = await sendMessageToAI({ message: newMessages, withRAG: false, prompt: rolePrompt });
setMessages([...newMessages, { role: 'ai', text: aiResponse }]);
} catch (error) {
console.error('发送失败:', error);
setMessages([...newMessages, { role: 'ai', text: '⚠️ 出现错误,请稍后重试。' }]);
}
};
用户输入消息后,发送给AI并显示回复,支持Markdown渲染。
步骤:
相关代码:
{messages.map((msg, index) => (
<View key={index} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'flex-start' }}>
{msg.role !== 'user' && <Image source={require('../assets/deepseek.png')} style={{ width: 30, height: 30 }} />}
<View style={{ flex: 1 }}>
<View style={[styles.messageContainer, msg.role === 'user' ? styles.userMessageContainer : styles.aiMessageContainer]}>
{msg.role === 'ai' ? <Markdown>{msg.text}</Markdown> : <Text style={styles.messageText}>{msg.text}</Text>}
</View>
</View>
{msg.role === 'user' && <Image source={require('../assets/bigmax.jpg')} style={{ width: 30, height: 30 }} />}
</View>
</View>
))}
提供了快速输入选项,用户可直接选择预设问题,输入框根据键盘状态自适应。
步骤:
相关代码:
useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => setKeyboardOn(true));
const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => setKeyboardOn(false));
return () => {
keyboardDidHideListener.remove();
keyboardDidShowListener.remove();
};
}, []);
<TouchableWithoutFeedback onPress={() => setSecondMenu('')}>
<View style={[isKeyboardOn ? styles.fastInputContainerOn : styles.fastInputContainer]}>
<Button title={'症状咨询'} onPress={() => setSecondMenu('a')} />
<Button title={'健康管理'} onPress={() => setSecondMenu('b')} />
</View>
</TouchableWithoutFeedback>
该功能实现了基于 React Navigation 的页面切换。主要分为两部分:
在应用启动时,我们需要检查本地存储(AsyncStorage
)中是否有用户信息。如果有用户信息,则表示用户已登录,直接进入主界面;如果没有,跳转到登录页面。
相关代码:
// 检查用户是否已登录
const checkLoginStatus = async () => {
const user = await AsyncStorage.getItem('user'); // 从本地获取用户信息
setIsLoggedIn(user !== null); // 如果有用户信息,说明已登录
};
使用 createNativeStackNavigator
来实现堆栈导航,将各个页面按顺序组织。登录页面、注册页面等都作为堆栈的一部分,用户可以通过堆栈导航进行切换。
相关代码:
// 堆栈导航 - 登录或主界面选择
return (
<NavigationContainer>
<Stack.Navigator initialRouteName={isLoggedIn ? "MainTabs" : "Login"}>
<Stack.Screen name="Login" component={LoginScreen} options={{ title: '登录' }} />
{/* 其他页面 */}
<Stack.Screen name="MainTabs" component={BottomTabNavigator} options={{ headerShown: false }} />
</Stack.Navigator>
</NavigationContainer>
);
一旦用户登录成功,就会跳转到包含底部导航栏的主界面。createBottomTabNavigator
用于实现底部的导航标签,用户可以快速访问不同功能模块,如首页、身体指标、计划填写等。
相关代码:
// 底部Tab导航 - 主界面功能模块
export default function BottomTabNavigator() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ color, size }) => {
let iconName = 'home';
if (route.name === 'Dashboard') iconName = 'home';
else if (route.name === 'BodyMetrics') iconName = 'fitness';
else if (route.name === 'PlanForm') iconName = 'calendar';
else if (route.name === 'ChatWithAI') iconName = 'chatbubble-ellipses';
else if (route.name === 'FoodAdvisor') iconName = 'restaurant';
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#2196F3', // 激活时颜色
tabBarInactiveTintColor: 'gray', // 非激活时颜色
})}
>
<Tab.Screen name="Dashboard" component={DashboardScreen} options={{ title: '首页' }} />
<Tab.Screen name="BodyMetrics" component={BodyMetricsScreen} options={{ title: '身体指标' }} />
<Tab.Screen name="PlanForm" component={PlanFormScreen} options={{ title: '计划填写' }} />
<Tab.Screen name="ChatWithAI" component={ChatWithAIScreen} options={{ title: 'AI助手' }} />
<Tab.Screen name="FoodAdvisor" component={FoodAdvisorScreen} options={{ title: '饮食建议' }} />
</Tab.Navigator>
);
}