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
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