This commit is contained in:
fuenmao
2026-10-09 16:46:35 +08:00
parent 4925c4c74d
commit 563836f789
60 changed files with 4729 additions and 2047 deletions
+44 -29
View File
@@ -1,8 +1,10 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Button as VanButton, showConfirmDialog, showToast } from 'vant'
import AppTabBar from '../components/AppTabBar.vue'
import { usePageRequest } from '../composables/usePageRequest'
import { ageFromBirthday } from '../services/formValidation'
import { clearTokens } from '../services/auth'
import { healthApi, type Dashboard, type HealthAssessmentOutput } from '../services/health'
@@ -10,34 +12,42 @@ const router = useRouter()
const loading = ref(true)
const dashboard = ref<Dashboard>()
const assessment = ref<HealthAssessmentOutput>()
const loadError = ref('')
const pageRequest = usePageRequest()
const logoutBusy = ref(false)
let active = true
async function loadData() {
const requestId = pageRequest.begin()
loading.value = true
loadError.value = ''
try {
const [dashRes, assessRes] = await Promise.all([
healthApi.dashboard(),
healthApi.getAssessment().catch(() => undefined)
healthApi.getAssessment()
])
if (!pageRequest.isCurrent(requestId)) return
if (!dashRes || !assessRes) throw new Error('健康档案暂时无法读取,请重试')
dashboard.value = dashRes
assessment.value = assessRes
} catch (err) {
showToast(err instanceof Error ? err.message : '加载失败')
if (pageRequest.isCurrent(requestId)) loadError.value = err instanceof Error ? err.message : '加载失败,请重试'
} finally {
loading.value = false
if (pageRequest.isCurrent(requestId)) loading.value = false
}
}
const bmiDisplay = computed(() => {
if (dashboard.value?.bmi) {
if (typeof dashboard.value?.bmi === 'number' && Number.isFinite(dashboard.value.bmi) && dashboard.value.bmi > 0) {
return {
val: dashboard.value.bmi,
status: dashboard.value.bmiStatus || '正常'
status: dashboard.value.bmiStatus || '未提供分类'
}
}
if (assessment.value?.bmi) {
if (typeof assessment.value?.bmi === 'number' && Number.isFinite(assessment.value.bmi) && assessment.value.bmi > 0) {
return {
val: assessment.value.bmi,
status: assessment.value.bmiStatus || '正常'
status: assessment.value.bmiStatus || '未提供分类'
}
}
return null
@@ -45,12 +55,7 @@ const bmiDisplay = computed(() => {
const ageDisplay = computed(() => {
const raw = dashboard.value?.profile?.birthday || assessment.value?.basicInfo.birthday
if (!raw) return undefined
const birthday = new Date(raw)
const today = new Date()
let age = today.getFullYear() - birthday.getFullYear()
if (today < new Date(today.getFullYear(), birthday.getMonth(), birthday.getDate())) age--
return age
return ageFromBirthday(raw)
})
// 健康评估只展示生活习惯画像;体质由中医四诊独立判定。
@@ -64,7 +69,7 @@ const profileTags = computed(() => {
list.push({ label: '夜间偏热感', desc: '自述夜间较容易感到闷热', type: 'blue' })
} else if (ans.temperaturePreference === 'cold_fear') {
list.push({ label: '夜间偏冷感', desc: '自述睡眠时较容易感到寒冷', type: 'gold' })
} else {
} else if (ans.temperaturePreference === 'normal') {
list.push({ label: '冷热感受适中', desc: '自述夜间温度感受较平稳', type: 'green' })
}
@@ -86,19 +91,23 @@ const profileTags = computed(() => {
})
function handleLogout() {
if (!active || logoutBusy.value) return
logoutBusy.value = true
showConfirmDialog({
title: '确认退出',
message: '确定要退出当前账号登录吗?'
}).then(() => {
if (!active) return
clearTokens()
showToast('已安全退出')
router.replace('/auth')
}).catch(() => {})
}).catch(() => {}).finally(() => { if (active) logoutBusy.value = false })
}
onMounted(() => {
loadData()
})
onUnmounted(() => { active = false })
</script>
<template>
@@ -110,7 +119,7 @@ onMounted(() => {
<span>{{ (dashboard?.user.displayName || '客').slice(0, 1) }}</span>
</div>
<div class="user-info">
<h2>{{ dashboard?.user.displayName || '尊贵会员' }}</h2>
<h2>{{ dashboard?.user.displayName || '我的健康档案' }}</h2>
<p class="phone-text">{{ dashboard?.user.phone || '健康管理账号' }}</p>
<span class="user-badge">常青树 · 智能健康睡眠会员</span>
</div>
@@ -118,17 +127,22 @@ onMounted(() => {
</header>
<main class="profile-content">
<section v-if="loading" class="profile-card page-load-state" role="status"><span class="page-loading-mark" aria-hidden="true"></span><p>正在读取您的健康档案…</p></section>
<section v-else-if="loadError" class="profile-card page-error-state" role="alert">
<h3>暂时无法读取健康档案</h3><p>{{ loadError }}</p><button class="retry-button" type="button" @click="loadData">重新加载</button>
</section>
<template v-else-if="dashboard">
<!-- 身体指标基线档案 -->
<section class="profile-card">
<div class="card-head">
<h3>身体基线档案</h3>
<button class="edit-btn" @click="router.replace('/assessment')">编辑指标</button>
<button class="edit-btn" type="button" @click="router.replace('/assessment')">编辑指标</button>
</div>
<div class="metrics-grid">
<div class="metric-item">
<span class="m-label">性别</span>
<strong class="m-val">{{ dashboard?.profile?.gender || assessment?.basicInfo.gender || '已保密' }}</strong>
<strong class="m-val">{{ dashboard?.profile?.gender || assessment?.basicInfo.gender || '未填写' }}</strong>
</div>
<div class="metric-item">
<span class="m-label">身高</span>
@@ -142,7 +156,7 @@ onMounted(() => {
<span class="m-label">体重</span>
<strong class="m-val">{{ dashboard?.profile?.weightKg || assessment?.basicInfo.weightKg || '--' }} <small>kg</small></strong>
</div>
<div class="metric-item">
<div class="metric-item bmi-metric">
<span class="m-label">BMI 指数</span>
<strong class="m-val" :class="{ highlight: bmiDisplay }">
{{ bmiDisplay?.val || '--' }}
@@ -155,7 +169,7 @@ onMounted(() => {
<!-- 专属评估画像标签 -->
<section class="profile-card">
<div class="card-head">
<h3>健康评估画像与专属策略</h3>
<h3>生活习惯画像</h3>
<span class="status-indicator" :class="{ done: assessment?.hasCompletedAssessment }">
{{ assessment?.hasCompletedAssessment ? '已完成评估' : '未评估' }}
</span>
@@ -208,8 +222,8 @@ onMounted(() => {
<!-- 当前健康方案 -->
<section class="profile-card" v-if="dashboard?.currentPlan">
<div class="card-head">
<h3>当前执行方案</h3>
<span class="version-tag">版本 v{{ dashboard.currentPlan.version }}.0</span>
<h3>日常健康建议</h3>
<span class="version-tag">版本 v{{ dashboard.currentPlan.version }}</span>
</div>
<p class="plan-summary-text">{{ dashboard.currentPlan.summary }}</p>
@@ -231,7 +245,7 @@ onMounted(() => {
<!-- 快捷功能入口与操作菜单 -->
<section class="action-card-menu">
<div class="menu-item" @click="router.replace('/assessment')">
<button class="menu-item" type="button" @click="router.replace('/assessment')">
<div class="menu-left">
<div class="menu-icon icon-assess">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
@@ -247,9 +261,9 @@ onMounted(() => {
</div>
</div>
<span class="arrow">&gt;</span>
</div>
</button>
<div class="menu-item" @click="router.push('/device-control')">
<button class="menu-item" type="button" @click="router.push('/device-control')">
<div class="menu-left">
<div class="menu-icon icon-device">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
@@ -263,16 +277,17 @@ onMounted(() => {
</div>
<div class="menu-text">
<strong>设备详情与参数调节</strong>
<small>查看7台设备状态、独立开关与精细参数设置</small>
<small>查看设备状态、快捷开关与完整参数</small>
</div>
</div>
<span class="arrow">&gt;</span>
</div>
</button>
</section>
</template>
<!-- 退出登录按钮 -->
<div class="logout-wrap">
<button class="logout-btn" @click="handleLogout">
<button class="logout-btn" type="button" :disabled="logoutBusy" @click="handleLogout">
退出登录
</button>
</div>