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.
 
 
 
 
 
 

15 KiB

07-个人中心页面

目标

实现个人中心和健康档案管理页面。


UI 设计参考

参考设计稿:files/ui/我的.png

页面布局

区域 设计要点
顶部 绿色背景 #10B981 + "我的" 标题(白色)
用户卡片 头像(圆形)+ 姓名 + 基本信息 + 用户ID + 编辑按钮
健康管理 "用药情况" 入口(带数量角标 12条
设置列表 消息通知、隐私设置、通用设置

用户卡片样式

元素 样式
头像 64px 圆形,浅绿色背景
姓名 白色,字号 20px
基本信息 白色,格式 "男·28岁·175cm·70kg"
用户ID 浅白色 rgba(255,255,255,0.7)
编辑按钮 白色半透明背景,编辑图标

列表项样式

元素 样式
图标背景 40px 圆形,各功能不同颜色
用药情况 绿色背景 #DCFCE7,图标 #10B981
消息通知 绿色背景 #DCFCE7,铃铛图标
隐私设置 绿色背景 #DCFCE7,盾牌图标
通用设置 绿色背景 #DCFCE7,齿轮图标
右箭头 灰色 #9CA3AF
角标 灰色文字 "12条"

前置要求

  • 前面所有页面完成
  • 后端用户接口可用

实施步骤

步骤 1:创建个人中心页面

创建 src/views/profile/Index.vue

<template>
  <div class="profile-page">
    <el-card class="user-card">
      <div class="user-info">
        <el-avatar :size="80" :src="userStore.userInfo?.avatar">
          {{ userStore.userInfo?.nickname?.charAt(0) || 'U' }}
        </el-avatar>
        <div class="user-detail">
          <h2>{{ userStore.userInfo?.nickname || '用户' }}</h2>
          <p>{{ userStore.userInfo?.phone }}</p>
        </div>
        <el-button type="primary" text @click="showEditDialog = true">
          编辑资料
        </el-button>
      </div>
    </el-card>

    <el-card class="menu-card">
      <div class="menu-list">
        <div class="menu-item" @click="$router.push('/health-record')">
          <div class="menu-icon">
            <el-icon><Document /></el-icon>
          </div>
          <div class="menu-content">
            <span class="menu-title">健康档案</span>
            <span class="menu-desc">查看和管理您的健康信息</span>
          </div>
          <el-icon><ArrowRight /></el-icon>
        </div>

        <div class="menu-item" @click="$router.push('/constitution/result')">
          <div class="menu-icon">
            <el-icon><User /></el-icon>
          </div>
          <div class="menu-content">
            <span class="menu-title">体质报告</span>
            <span class="menu-desc">查看您的体质辨识结果</span>
          </div>
          <el-icon><ArrowRight /></el-icon>
        </div>

        <div class="menu-item" @click="$router.push('/constitution')">
          <div class="menu-icon">
            <el-icon><Refresh /></el-icon>
          </div>
          <div class="menu-content">
            <span class="menu-title">重新测评</span>
            <span class="menu-desc">建议每3-6个月重新测评一次</span>
          </div>
          <el-icon><ArrowRight /></el-icon>
        </div>

        <div class="menu-item" @click="showAbout">
          <div class="menu-icon">
            <el-icon><InfoFilled /></el-icon>
          </div>
          <div class="menu-content">
            <span class="menu-title">关于我们</span>
            <span class="menu-desc">了解健康AI助手</span>
          </div>
          <el-icon><ArrowRight /></el-icon>
        </div>
      </div>
    </el-card>

    <div class="logout-container">
      <el-button type="danger" text @click="handleLogout">
        退出登录
      </el-button>
    </div>

    <!-- 编辑资料对话框 -->
    <el-dialog v-model="showEditDialog" title="编辑资料" width="400px">
      <el-form :model="editForm" label-width="80px">
        <el-form-item label="昵称">
          <el-input v-model="editForm.nickname" placeholder="请输入昵称" />
        </el-form-item>
        <el-form-item label="邮箱">
          <el-input v-model="editForm.email" placeholder="请输入邮箱" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showEditDialog = false">取消</el-button>
        <el-button type="primary" :loading="saving" @click="saveProfile">
          保存
        </el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
  Document,
  User,
  Refresh,
  InfoFilled,
  ArrowRight,
} from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
import { updateUserProfile } from '@/api/user'

const router = useRouter()
const userStore = useUserStore()

const showEditDialog = ref(false)
const saving = ref(false)
const editForm = reactive({
  nickname: '',
  email: '',
})

watch(showEditDialog, (val) => {
  if (val && userStore.userInfo) {
    editForm.nickname = userStore.userInfo.nickname || ''
    editForm.email = userStore.userInfo.email || ''
  }
})

const saveProfile = async () => {
  saving.value = true
  try {
    await updateUserProfile(editForm)
    userStore.userInfo.nickname = editForm.nickname
    userStore.userInfo.email = editForm.email
    ElMessage.success('保存成功')
    showEditDialog.value = false
  } catch (error) {
    // 错误已处理
  } finally {
    saving.value = false
  }
}

const showAbout = () => {
  ElMessageBox.alert(
    `健康AI助手是一款智能健康咨询应用,结合中医体质辨识理论,为您提供个性化的健康建议。

版本:1.0.0
开发者:Health AI Team`,
    '关于我们',
    { confirmButtonText: '我知道了' }
  )
}

const handleLogout = () => {
  ElMessageBox.confirm('确定要退出登录吗?', '提示', {
    type: 'warning',
  }).then(() => {
    userStore.logout()
    router.push('/auth/login')
  })
}
</script>

<style scoped>
.profile-page {
  max-width: 600px;
  margin: 0 auto;
}

.user-card {
  margin-bottom: 20px;
}

.user-info {
  display: flex;
  align-items: center;
}

.user-detail {
  flex: 1;
  margin-left: 20px;
}

.user-detail h2 {
  margin-bottom: 4px;
  font-size: 20px;
}

.user-detail p {
  color: #999;
  font-size: 14px;
}

.menu-card {
  margin-bottom: 20px;
}

.menu-list {
  margin: -20px;
}

.menu-item {
  display: flex;
  align-items: center;
  padding: 16px 20px;
  border-bottom: 1px solid #f0f0f0;
  cursor: pointer;
  transition: background 0.2s;
}

.menu-item:hover {
  background: #f9f9f9;
}

.menu-item:last-child {
  border-bottom: none;
}

.menu-icon {
  width: 40px;
  height: 40px;
  background: #ecf5ff;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 16px;
}

.menu-icon .el-icon {
  font-size: 20px;
  color: #409eff;
}

.menu-content {
  flex: 1;
}

.menu-title {
  display: block;
  font-size: 15px;
  color: #333;
  margin-bottom: 4px;
}

.menu-desc {
  font-size: 12px;
  color: #999;
}

.logout-container {
  text-align: center;
  padding: 20px 0;
}
</style>

步骤 2:创建健康档案页面

创建 src/views/profile/HealthRecord.vue

<template>
  <div class="health-record-page">
    <div class="page-header">
      <el-button text @click="$router.back()">
        <el-icon><ArrowLeft /></el-icon>
        返回
      </el-button>
      <h2>健康档案</h2>
    </div>

    <div v-if="loading" class="loading">
      <el-skeleton :rows="10" animated />
    </div>

    <template v-else>
      <!-- 基础信息 -->
      <el-card class="section-card">
        <template #header>
          <div class="card-header">
            <span>基础信息</span>
            <el-button type="primary" text @click="editBasicInfo">编辑</el-button>
          </div>
        </template>

        <el-descriptions :column="2" border v-if="profile.basic_info">
          <el-descriptions-item label="姓名">{{ profile.basic_info.name || '-' }}</el-descriptions-item>
          <el-descriptions-item label="性别">{{ genderMap[profile.basic_info.gender] || '-' }}</el-descriptions-item>
          <el-descriptions-item label="身高">{{ profile.basic_info.height ? profile.basic_info.height + ' cm' : '-' }}</el-descriptions-item>
          <el-descriptions-item label="体重">{{ profile.basic_info.weight ? profile.basic_info.weight + ' kg' : '-' }}</el-descriptions-item>
          <el-descriptions-item label="BMI">{{ profile.basic_info.bmi ? profile.basic_info.bmi.toFixed(1) : '-' }}</el-descriptions-item>
          <el-descriptions-item label="血型">{{ profile.basic_info.blood_type || '-' }}</el-descriptions-item>
          <el-descriptions-item label="职业">{{ profile.basic_info.occupation || '-' }}</el-descriptions-item>
          <el-descriptions-item label="地区">{{ profile.basic_info.region || '-' }}</el-descriptions-item>
        </el-descriptions>

        <el-empty v-else description="暂无基础信息" />
      </el-card>

      <!-- 体质信息 -->
      <el-card class="section-card">
        <template #header>
          <div class="card-header">
            <span>体质信息</span>
            <el-button type="primary" text @click="$router.push('/constitution')">重新测评</el-button>
          </div>
        </template>

        <div v-if="profile.constitution" class="constitution-info">
          <div class="constitution-main">
            <el-tag size="large" type="success">{{ profile.constitution.primary_name }}</el-tag>
            <p>{{ profile.constitution.primary_description }}</p>
          </div>
          <div class="constitution-time">
            最近测评时间:{{ profile.constitution.assessed_at }}
          </div>
        </div>

        <el-empty v-else description="暂无体质测评记录">
          <el-button type="primary" @click="$router.push('/constitution')">
            立即测评
          </el-button>
        </el-empty>
      </el-card>

      <!-- 生活习惯 -->
      <el-card class="section-card">
        <template #header>
          <div class="card-header">
            <span>生活习惯</span>
            <el-button type="primary" text @click="editLifestyle">编辑</el-button>
          </div>
        </template>

        <el-descriptions :column="2" border v-if="profile.lifestyle">
          <el-descriptions-item label="入睡时间">{{ profile.lifestyle.sleep_time || '-' }}</el-descriptions-item>
          <el-descriptions-item label="起床时间">{{ profile.lifestyle.wake_time || '-' }}</el-descriptions-item>
          <el-descriptions-item label="睡眠质量">{{ sleepQualityMap[profile.lifestyle.sleep_quality] || '-' }}</el-descriptions-item>
          <el-descriptions-item label="运动频率">{{ exerciseFreqMap[profile.lifestyle.exercise_frequency] || '-' }}</el-descriptions-item>
          <el-descriptions-item label="吸烟">{{ profile.lifestyle.is_smoker ? '是' : '否' }}</el-descriptions-item>
          <el-descriptions-item label="饮酒">{{ alcoholMap[profile.lifestyle.alcohol_frequency] || '-' }}</el-descriptions-item>
        </el-descriptions>

        <el-empty v-else description="暂无生活习惯信息" />
      </el-card>

      <!-- 病史记录 -->
      <el-card class="section-card">
        <template #header>
          <div class="card-header">
            <span>既往病史</span>
          </div>
        </template>

        <div v-if="profile.medical_history?.length" class="tag-list">
          <el-tag v-for="item in profile.medical_history" :key="item.id" size="large">
            {{ item.disease_name }}
          </el-tag>
        </div>
        <el-empty v-else description="暂无病史记录" />
      </el-card>

      <!-- 过敏信息 -->
      <el-card class="section-card">
        <template #header>
          <div class="card-header">
            <span>过敏信息</span>
          </div>
        </template>

        <div v-if="profile.allergy_records?.length" class="tag-list">
          <el-tag
            v-for="item in profile.allergy_records"
            :key="item.id"
            size="large"
            type="danger"
          >
            {{ item.allergen }}({{ allergyTypeMap[item.allergy_type] }})
          </el-tag>
        </div>
        <el-empty v-else description="暂无过敏信息" />
      </el-card>
    </template>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ArrowLeft } from '@element-plus/icons-vue'
import { getHealthProfile } from '@/api/user'

const router = useRouter()

const loading = ref(true)
const profile = ref<any>({})

const genderMap: Record<string, string> = {
  male: '男',
  female: '女',
}

const sleepQualityMap: Record<string, string> = {
  good: '好',
  normal: '一般',
  poor: '差',
}

const exerciseFreqMap: Record<string, string> = {
  never: '从不',
  sometimes: '偶尔',
  often: '经常',
  daily: '每天',
}

const alcoholMap: Record<string, string> = {
  never: '从不',
  sometimes: '偶尔',
  often: '经常',
}

const allergyTypeMap: Record<string, string> = {
  drug: '药物',
  food: '食物',
  other: '其他',
}

onMounted(async () => {
  try {
    profile.value = await getHealthProfile()
  } catch (error) {
    // 错误已处理
  } finally {
    loading.value = false
  }
})

const editBasicInfo = () => {
  router.push('/survey')
}

const editLifestyle = () => {
  router.push('/survey')
}
</script>

<style scoped>
.health-record-page {
  max-width: 800px;
  margin: 0 auto;
}

.page-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 20px;
}

.page-header h2 {
  margin: 0;
}

.loading {
  background: #fff;
  padding: 20px;
  border-radius: 8px;
}

.section-card {
  margin-bottom: 20px;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.constitution-info {
  text-align: center;
}

.constitution-main {
  margin-bottom: 16px;
}

.constitution-main p {
  margin-top: 12px;
  color: #666;
}

.constitution-time {
  font-size: 13px;
  color: #999;
}

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
</style>

需要创建的文件清单

文件路径 说明
src/views/profile/Index.vue 个人中心页面
src/views/profile/HealthRecord.vue 健康档案页面

验收标准

  • 个人中心显示用户信息
  • 编辑资料功能正常
  • 健康档案数据正确显示
  • 各菜单跳转正常
  • 退出登录功能正常

预计耗时

25-30 分钟


下一步

Web 前端开发完成!进入 04-APP开发/01-项目结构初始化.md