You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
12 KiB
12 KiB
07-个人中心页面
目标
实现 APP 端个人中心和健康档案管理页面。
UI 设计参考
参考设计稿:
files/ui/我的.png
页面布局
| 区域 | 设计要点 |
|---|---|
| 顶部 | 绿色背景 #10B981 + "我的" 标题(白色) |
| 用户卡片 | 头像(64px 圆形)+ 姓名 + 基本信息 + 用户ID |
| 编辑按钮 | 白色半透明背景,编辑图标 |
| 健康管理 | "用药情况" 入口(带角标 "12条") |
| 设置列表 | 消息通知、隐私设置、通用设置 |
用户卡片样式
const userCardStyles = {
container: {
backgroundColor: '#10B981',
padding: 20,
borderRadius: 16,
},
avatar: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: 'rgba(255,255,255,0.2)',
},
nickname: {
color: '#FFFFFF',
fontSize: 20,
fontWeight: '600',
},
basicInfo: {
color: '#FFFFFF',
fontSize: 14,
},
userId: {
color: 'rgba(255,255,255,0.7)',
fontSize: 12,
},
}
列表项样式
| 元素 | 样式 |
|---|---|
| 图标背景 | 40px 圆形 |
| 用药情况 | #DCFCE7 背景,#10B981 图标 |
| 消息通知 | #DCFCE7 背景,铃铛图标 |
| 隐私设置 | #DCFCE7 背景,盾牌图标 |
| 通用设置 | #DCFCE7 背景,齿轮图标 |
| 角标 | 灰色文字 #6B7280 |
| 右箭头 | 灰色 #9CA3AF |
const listItemStyles = {
container: {
backgroundColor: '#FFFFFF',
paddingVertical: 16,
paddingHorizontal: 16,
borderRadius: 12,
},
iconContainer: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#DCFCE7',
justifyContent: 'center',
alignItems: 'center',
},
iconColor: '#10B981',
title: {
fontSize: 16,
color: '#1F2937',
},
description: {
fontSize: 13,
color: '#6B7280',
},
badge: {
fontSize: 13,
color: '#6B7280',
},
}
实现要点
- 用户信息卡片:显示头像、昵称、手机号
- 功能菜单列表:使用 List 组件展示菜单项
- 健康档案:展示基础信息、体质、病史等
关键代码示例
个人中心页面
// src/screens/profile/ProfileHomeScreen.tsx
import React from 'react'
import { View, ScrollView, StyleSheet, Alert } from 'react-native'
import { Text, Avatar, Card, List, Button, Divider } from 'react-native-paper'
import { useNavigation } from '@react-navigation/native'
import { useUserStore } from '../../stores/userStore'
import type { ProfileStackParamList } from '../../navigation/types'
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
type NavigationProp = NativeStackNavigationProp<ProfileStackParamList>
const ProfileHomeScreen = () => {
const navigation = useNavigation<NavigationProp>()
const { user, logout } = useUserStore()
const handleLogout = () => {
Alert.alert('提示', '确定要退出登录吗?', [
{ text: '取消', style: 'cancel' },
{
text: '确定',
style: 'destructive',
onPress: () => logout(),
},
])
}
return (
<ScrollView style={styles.container}>
{/* 用户信息卡片 */}
<Card style={styles.userCard}>
<Card.Content style={styles.userContent}>
<Avatar.Text
size={64}
label={user?.nickname?.charAt(0) || 'U'}
style={styles.avatar}
/>
<View style={styles.userInfo}>
<Text style={styles.nickname}>{user?.nickname || '用户'}</Text>
<Text style={styles.phone}>{user?.phone}</Text>
</View>
</Card.Content>
</Card>
{/* 功能菜单 */}
<Card style={styles.menuCard}>
<List.Item
title="健康档案"
description="查看和管理您的健康信息"
left={(props) => <List.Icon {...props} icon="file-document" />}
right={(props) => <List.Icon {...props} icon="chevron-right" />}
onPress={() => navigation.navigate('HealthRecord')}
/>
<Divider />
<List.Item
title="体质报告"
description="查看您的体质辨识结果"
left={(props) => <List.Icon {...props} icon="chart-line" />}
right={(props) => <List.Icon {...props} icon="chevron-right" />}
onPress={() => navigation.getParent()?.navigate('ConstitutionTab')}
/>
<Divider />
<List.Item
title="重新测评"
description="建议每3-6个月重新测评一次"
left={(props) => <List.Icon {...props} icon="refresh" />}
right={(props) => <List.Icon {...props} icon="chevron-right" />}
onPress={() => navigation.getParent()?.navigate('ConstitutionTab', {
screen: 'ConstitutionQuestions',
})}
/>
<Divider />
<List.Item
title="关于我们"
description="了解健康AI助手"
left={(props) => <List.Icon {...props} icon="information" />}
right={(props) => <List.Icon {...props} icon="chevron-right" />}
onPress={() =>
Alert.alert(
'关于我们',
'健康AI助手是一款智能健康咨询应用,结合中医体质辨识理论,为您提供个性化的健康建议。\n\n版本:1.0.0'
)
}
/>
</Card>
{/* 退出登录 */}
<View style={styles.logoutContainer}>
<Button mode="text" textColor="#f56c6c" onPress={handleLogout}>
退出登录
</Button>
</View>
</ScrollView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
userCard: {
margin: 16,
},
userContent: {
flexDirection: 'row',
alignItems: 'center',
},
avatar: {
backgroundColor: '#667eea',
},
userInfo: {
marginLeft: 16,
},
nickname: {
fontSize: 20,
fontWeight: 'bold',
},
phone: {
color: '#999',
marginTop: 4,
},
menuCard: {
marginHorizontal: 16,
},
logoutContainer: {
padding: 24,
alignItems: 'center',
},
})
export default ProfileHomeScreen
健康档案页面
// src/screens/profile/HealthRecordScreen.tsx
import React, { useState, useEffect } from 'react'
import { View, ScrollView, StyleSheet } from 'react-native'
import { Text, Card, Chip, ActivityIndicator } from 'react-native-paper'
import { getHealthProfile } from '../../api/user'
const genderMap: Record<string, string> = {
male: '男',
female: '女',
}
const HealthRecordScreen = () => {
const [profile, setProfile] = useState<any>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
loadProfile()
}, [])
const loadProfile = async () => {
try {
const data = await getHealthProfile()
setProfile(data)
} finally {
setLoading(false)
}
}
if (loading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" />
</View>
)
}
return (
<ScrollView style={styles.container}>
{/* 基础信息 */}
<Card style={styles.card}>
<Card.Title title="基础信息" />
<Card.Content>
{profile?.basic_info ? (
<View style={styles.infoGrid}>
<InfoItem label="姓名" value={profile.basic_info.name} />
<InfoItem
label="性别"
value={genderMap[profile.basic_info.gender]}
/>
<InfoItem
label="身高"
value={profile.basic_info.height ? `${profile.basic_info.height} cm` : '-'}
/>
<InfoItem
label="体重"
value={profile.basic_info.weight ? `${profile.basic_info.weight} kg` : '-'}
/>
<InfoItem
label="BMI"
value={profile.basic_info.bmi?.toFixed(1)}
/>
<InfoItem label="血型" value={profile.basic_info.blood_type} />
</View>
) : (
<Text style={styles.emptyText}>暂无基础信息</Text>
)}
</Card.Content>
</Card>
{/* 体质信息 */}
<Card style={styles.card}>
<Card.Title title="体质信息" />
<Card.Content>
{profile?.constitution ? (
<View style={styles.constitutionInfo}>
<Chip style={styles.constitutionChip}>
{profile.constitution.primary_name}
</Chip>
<Text style={styles.constitutionDesc}>
{profile.constitution.primary_description}
</Text>
<Text style={styles.assessedTime}>
测评时间:{profile.constitution.assessed_at}
</Text>
</View>
) : (
<Text style={styles.emptyText}>暂无体质测评记录</Text>
)}
</Card.Content>
</Card>
{/* 既往病史 */}
<Card style={styles.card}>
<Card.Title title="既往病史" />
<Card.Content>
{profile?.medical_history?.length > 0 ? (
<View style={styles.tagList}>
{profile.medical_history.map((item: any) => (
<Chip key={item.id} style={styles.tag}>
{item.disease_name}
</Chip>
))}
</View>
) : (
<Text style={styles.emptyText}>暂无病史记录</Text>
)}
</Card.Content>
</Card>
{/* 过敏信息 */}
<Card style={styles.card}>
<Card.Title title="过敏信息" />
<Card.Content>
{profile?.allergy_records?.length > 0 ? (
<View style={styles.tagList}>
{profile.allergy_records.map((item: any) => (
<Chip key={item.id} style={[styles.tag, styles.allergyTag]}>
{item.allergen}
</Chip>
))}
</View>
) : (
<Text style={styles.emptyText}>暂无过敏信息</Text>
)}
</Card.Content>
</Card>
</ScrollView>
)
}
const InfoItem = ({ label, value }: { label: string; value?: string }) => (
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>{label}</Text>
<Text style={styles.infoValue}>{value || '-'}</Text>
</View>
)
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 16,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
card: {
marginBottom: 16,
},
infoGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
},
infoItem: {
width: '50%',
marginBottom: 12,
},
infoLabel: {
fontSize: 13,
color: '#999',
marginBottom: 4,
},
infoValue: {
fontSize: 15,
},
emptyText: {
color: '#999',
textAlign: 'center',
padding: 16,
},
constitutionInfo: {
alignItems: 'center',
},
constitutionChip: {
backgroundColor: '#667eea',
marginBottom: 12,
},
constitutionDesc: {
color: '#666',
textAlign: 'center',
lineHeight: 22,
},
assessedTime: {
marginTop: 12,
fontSize: 12,
color: '#999',
},
tagList: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
tag: {
marginBottom: 8,
},
allergyTag: {
backgroundColor: '#fef0f0',
},
})
export default HealthRecordScreen
需要创建的文件
| 文件路径 | 说明 |
|---|---|
src/api/user.ts |
用户 API |
src/screens/profile/ProfileHomeScreen.tsx |
个人中心 |
src/screens/profile/HealthRecordScreen.tsx |
健康档案 |
验收标准
- 用户信息正确显示
- 菜单导航正常
- 健康档案数据完整
- 退出登录功能正常
预计耗时
25-30 分钟
完成
恭喜!APP 端开发任务全部完成!
后续工作
- 测试:在真机和模拟器上进行完整功能测试
- 优化:性能优化、动画效果、错误处理
- 打包:
- Android:
cd android && ./gradlew assembleRelease - iOS: 使用 Xcode Archive
- Android:
- 发布:提交到应用商店审核