healthapp
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

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',
  },
}

实现要点

  1. 用户信息卡片:显示头像、昵称、手机号
  2. 功能菜单列表:使用 List 组件展示菜单项
  3. 健康档案:展示基础信息、体质、病史等

关键代码示例

个人中心页面

// 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 端开发任务全部完成!


后续工作

  1. 测试:在真机和模拟器上进行完整功能测试
  2. 优化:性能优化、动画效果、错误处理
  3. 打包
    • Android: cd android && ./gradlew assembleRelease
    • iOS: 使用 Xcode Archive
  4. 发布:提交到应用商店审核