feat: initialize Evergreen Health mobile app

This commit is contained in:
fuenmao
2026-09-21 15:53:02 +08:00
commit 93e9bb604d
18 changed files with 2715 additions and 0 deletions
+256
View File
@@ -0,0 +1,256 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Button as VanButton, showConfirmDialog, showToast } from 'vant'
import { clearTokens } from '../services/auth'
import { healthApi, type Dashboard } from '../services/health'
const router = useRouter()
const loading = ref(true)
const generating = ref(false)
const dashboard = ref<Dashboard>()
const moduleNames: Record<string, { name: string; icon: string }> = {
sleep: { name: '睡眠', icon: '🌙' },
exercise: { name: '运动', icon: '🏃' },
diet: { name: '饮食', icon: '🥗' }
}
const greetingInfo = computed(() => {
const hour = new Date().getHours()
if (hour < 11) return { text: '早安', icon: '🌅' }
if (hour < 14) return { text: '午安', icon: '☀️' }
if (hour < 19) return { text: '下午好', icon: '🍵' }
return { text: '晚上好', icon: '🌙' }
})
const todayDateText = computed(() => {
return new Intl.DateTimeFormat('zh-CN', {
month: 'long',
day: 'numeric',
weekday: 'short'
}).format(new Date())
})
async function load() {
loading.value = true
try {
dashboard.value = await healthApi.dashboard()
} catch (error) {
showToast(error instanceof Error ? error.message : '加载失败')
} finally {
loading.value = false
}
}
async function generate() {
generating.value = true
try {
await healthApi.generatePlan()
await load()
showToast({ type: 'success', message: '方案已生成并更新' })
} catch (error) {
showToast(error instanceof Error ? error.message : '生成失败')
} finally {
generating.value = false
}
}
function handleUserClick() {
showConfirmDialog({
title: '账户设置',
message: `当前登录:${dashboard.value?.user.displayName || '健康用户'}(${dashboard.value?.user.phone || ''}),确定要退出登录吗?`,
confirmButtonText: '退出登录',
cancelButtonText: '取消',
confirmButtonColor: '#c0392b'
}).then(() => {
clearTokens()
router.replace('/auth')
}).catch(() => {})
}
onMounted(load)
</script>
<template>
<main class="page-shell dashboard-page">
<!-- 顶部状态栏与用户头像 -->
<header class="dashboard-header">
<div class="header-meta">
<span class="eyebrow">EVERGREEN · TODAY</span>
<h1>{{ greetingInfo.icon }} {{ greetingInfo.text }},{{ dashboard?.user.displayName || '您好' }}</h1>
<div class="date-badge">
<span>📅 {{ todayDateText }}</span>
<span>· 一人一方每日协同</span>
</div>
</div>
<button class="user-chip" type="button" @click="handleUserClick">
<div class="avatar-badge">
{{ dashboard?.user.displayName?.slice(0, 1) || '常' }}
</div>
<span class="user-chip-name">我的账户</span>
</button>
</header>
<!-- 加载态骨架 -->
<section v-if="loading" class="section-card loading-card" style="padding: 40px 20px;">
<div style="font-size: 24px; margin-bottom: 10px;">🌿</div>
<p style="color: var(--text-sub); font-size: 14px;">正在同步您的今日健康方案与微环境参数…</p>
</section>
<template v-else>
<!-- 今日方案主卡片 (Hero Card) -->
<section class="hero-card">
<div class="hero-top-row">
<span class="eyebrow" style="color: #f1c37a;">今日专属干预方案</span>
<span v-if="dashboard?.currentPlan" style="font-size: 11px; padding: 2px 8px; border-radius: 10px; background: rgba(255,255,255,0.16); color: #fff;">
V{{ dashboard.currentPlan.version }} · 方案生效中
</span>
</div>
<h2>{{ dashboard?.currentPlan?.title || '尚未生成健康方案' }}</h2>
<p class="hero-summary">
{{ dashboard?.currentPlan?.summary || '完成基础板块选择与身体数据录入后,系统将为您量身定制每日干预方案。' }}
</p>
<!-- 启用的健康板块标签 -->
<div class="tag-row">
<span v-for="code in dashboard?.selectedModules" :key="code" class="tag-pill">
{{ moduleNames[code]?.icon }} {{ moduleNames[code]?.name || code }}
</span>
</div>
<!-- 操作按钮行 -->
<div class="hero-action-row">
<van-button
v-if="!dashboard?.currentPlan"
round
color="#f1c37a"
text-color="#183b34"
style="font-weight: 700;"
:loading="generating"
@click="generate"
>
✨ 立即生成专属方案
</van-button>
<van-button
v-else
round
size="small"
color="rgba(255,255,255,0.18)"
style="border: 1px solid rgba(255,255,255,0.3); color: #fff;"
:loading="generating"
@click="generate"
>
↺ 重新评估生成
</van-button>
</div>
</section>
<!-- 核心指标状态栏 (3 列式卡片) -->
<section class="metric-strip">
<div class="metric-card">
<span class="metric-label">🌙 睡眠目标</span>
<strong>7–8<em>h</em></strong>
<span class="metric-desc">建议 23:00 前</span>
</div>
<div class="metric-card">
<span class="metric-label">🏃 活动目标</span>
<strong>30<em>min</em></strong>
<span class="metric-desc">中等强度</span>
</div>
<div class="metric-card">
<span class="metric-label">🔖 方案版本</span>
<strong>V{{ dashboard?.currentPlan?.version || 0 }}<em>版</em></strong>
<span class="metric-desc">动态演进</span>
</div>
</section>
<!-- 今日六维行动建议 -->
<section class="section-card">
<div class="section-title">
<span>今日执行清单</span>
<button type="button" @click="router.push('/onboarding')">
调整偏好 ›
</button>
</div>
<div v-if="dashboard?.currentPlan?.items?.length">
<article
v-for="item in dashboard.currentPlan.items"
:key="item.moduleCode"
class="plan-item"
>
<div class="plan-item-icon">
{{ item.moduleCode === 'sleep' ? '🌙' : item.moduleCode === 'exercise' ? '🏃' : '🥗' }}
</div>
<div class="plan-item-content">
<div class="plan-item-header">
<strong>{{ item.title }}</strong>
<span class="plan-item-target">{{ item.target }}</span>
</div>
<p class="plan-item-advice">{{ item.advice }}</p>
</div>
</article>
</div>
<div v-else style="text-align: center; padding: 20px; color: var(--text-muted); font-size: 13px;">
暂无执行建议,点击上方按钮即可一键生成方案。
</div>
</section>
<!-- 五感微环境空间联动卡片 -->
<section class="section-card smart-space-card">
<div class="section-title">
<span>五感舒睡空间</span>
<small class="smart-badge">⚡ Home Assistant 待接入</small>
</div>
<div class="smart-scene-box">
<div class="scene-icon-title">
<div class="scene-icon">🛏️</div>
<div class="scene-text">
<strong>深度舒睡模式</strong>
<p>预冷20℃ · 琥珀微光 · 新风正压 · 草本扩香</p>
</div>
</div>
<button
type="button"
disabled
style="padding: 6px 10px; border-radius: 12px; font-size: 11px; background: #e8eae6; color: #8e9a95;"
>
准备就绪
</button>
</div>
</section>
<!-- 免责与合规提示 -->
<p class="medical-disclaimer">
⚠️ 本建议由一人一方生活方式决策模型生成,仅用于日常健康管理与微环境优化,不构成医疗诊断或临床处方。
</p>
</template>
<!-- 底部固定快捷导航栏 -->
<nav class="sticky-action" style="display: flex; gap: 8px; justify-content: space-between;">
<van-button
block
round
color="#0c4d3f"
style="flex: 2; height: 46px; font-size: 14px;"
@click="generate"
:loading="generating"
>
✨ 刷新今日建议
</van-button>
<van-button
round
plain
color="#0c4d3f"
style="flex: 1; height: 46px; font-size: 14px;"
@click="router.push('/onboarding')"
>
调整偏好
</van-button>
</nav>
</main>
</template>