Files
Evergreen-Health-Mobile/src/views/DashboardView.vue
T
2026-09-23 18:19:47 +08:00

196 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Button as VanButton, showToast } from 'vant'
import AppTabBar from '../components/AppTabBar.vue'
import { clearTokens } from '../services/auth'
import {
healthApi,
type Dashboard,
type HomeAssistantStatus
} from '../services/health'
const router = useRouter()
const loading = ref(true)
const sceneRunning = ref<'sleep' | 'all-off'>()
const dashboard = ref<Dashboard>()
const homeAssistant = ref<HomeAssistantStatus>()
const greeting = computed(() => {
const hour = new Date().getHours()
if (hour < 11) return '早上好'
if (hour < 18) return '下午好'
return '晚上好'
})
const sceneAvailable = computed(() => homeAssistant.value?.sceneDevices?.available ?? 0)
async function load() {
loading.value = true
try {
const [dashboardResult, homeAssistantResult] = await Promise.all([
healthApi.dashboard(),
healthApi.homeAssistantStatus().catch(() => undefined)
])
dashboard.value = dashboardResult
homeAssistant.value = homeAssistantResult
} catch (error) {
showToast(error instanceof Error ? error.message : '加载失败')
} finally {
loading.value = false
}
}
async function executeScene(scene: 'sleep' | 'all-off') {
if (scene === 'sleep' && !dashboard.value?.tcmResult?.hasCompleted) {
showToast('请先完成中医四诊,生成体质倾向')
router.push('/tcm-assessment')
return
}
sceneRunning.value = scene
try {
const result = scene === 'sleep'
? await healthApi.executeSleepScene()
: await healthApi.executeAllOffScene()
showToast({ type: result.success ? 'success' : 'fail', message: result.message })
homeAssistant.value = await healthApi.homeAssistantStatus()
} catch (error) {
showToast(error instanceof Error ? error.message : '场景执行失败')
} finally {
sceneRunning.value = undefined
}
}
function logout() {
clearTokens()
router.replace('/auth')
}
onMounted(load)
</script>
<template>
<main class="page-shell dashboard-page">
<header class="dashboard-header">
<div>
<p class="eyebrow">EVERGREEN · TODAY</p>
<h1>{{ greeting }},{{ dashboard?.user.displayName || '您好' }}</h1>
<p>{{ new Intl.DateTimeFormat('zh-CN', { month: 'long', day: 'numeric', weekday: 'long' }).format(new Date()) }}</p>
</div>
<button class="avatar-button" type="button" @click="logout">
{{ dashboard?.user.displayName?.slice(0, 1) || '常' }}
</button>
</header>
<section v-if="loading" class="section-card loading-card">正在读取今日健康方案…</section>
<template v-else>
<section class="hero-card">
<p class="eyebrow">今日健康建议</p>
<h2>{{ dashboard?.currentPlan?.title || '尚未完成健康评估' }}</h2>
<p>{{ dashboard?.currentPlan?.summary || '完成健康评估后,系统会整理您的基本情况并生成日常健康建议。' }}</p>
<van-button
v-if="!dashboard?.currentPlan"
round
color="#f1c37a"
text-color="#183b34"
@click="router.replace('/assessment')"
>
去健康评估
</van-button>
</section>
<!-- 未完成评估提示横幅 -->
<section v-if="!dashboard?.hasCompletedAssessment" class="assessment-guide-banner" @click="router.replace('/assessment')">
<div class="guide-badge">重要提醒</div>
<div class="guide-body">
<div class="guide-text">
<h3>完善个人基本信息与健康评估</h3>
<p>尚未记录身体及睡眠质量、运动情况、饮食情况。健康评估仅用于了解基本情况;设备启动参数由中医四诊体质结果生成。</p>
</div>
<button class="guide-btn" type="button">去评估 &gt;</button>
</div>
</section>
<!-- 智能家居启动参数仅来源于中医四诊体质结果 -->
<section class="section-card personalized-startup-card">
<div class="section-title">
<span>体质专属舒睡启动参数</span>
</div>
<p class="startup-card-desc">
{{ dashboard?.tcmResult?.hasCompleted
? `依据${dashboard.tcmResult.constitutionName}倾向生成基础预设;床垫加热看重庆天气,软硬度看 BMI。`
: '先完成中医四诊并生成体质倾向,才可一键开启专属场景。' }}
<a v-if="!dashboard?.tcmResult?.hasCompleted" href="/tcm-assessment" @click.prevent="router.push('/tcm-assessment')">去完成四诊 ›</a>
</p>
<div v-if="dashboard?.tcmResult?.hasCompleted" class="startup-params-tags">
<div class="startup-param-badge">
<span class="lbl">恒湿目标</span>
<strong>{{ dashboard?.sceneConfig?.targetHumidity || 55 }}% · {{ dashboard?.sceneConfig?.fanLevel || '1挡' }}</strong>
</div>
<div class="startup-param-badge">
<span class="lbl">助眠灯光</span>
<strong>50%渐暗至关闭 · {{ dashboard?.sceneConfig?.lightColorTemperatureKelvin || 2700 }}K</strong>
</div>
<div class="startup-param-badge">
<span class="lbl">助眠音箱</span>
<strong>{{ dashboard?.sceneConfig?.enableSpeakerSleepMode ? '开启睡眠模式' : '关闭睡眠模式' }} · {{ dashboard?.sceneConfig?.speakerVolumePercent || 15 }}%</strong>
</div>
<div class="startup-param-badge">
<span class="lbl">8H 床垫</span>
<strong>左右加热 {{ dashboard?.mattressComfort?.heatingLevel ?? 0 }} 档 · 软硬度{{ dashboard?.mattressComfort?.firmness || '待填写 BMI' }}</strong>
</div>
<div class="startup-param-badge" v-if="dashboard?.sceneConfig?.enableUv">
<span class="lbl">除湿消杀</span>
<strong style="color: #7b42b5;">UV 深度消杀已开启</strong>
</div>
</div>
<p v-if="dashboard?.tcmResult?.hasCompleted" class="startup-card-desc">
{{ dashboard?.mattressComfort?.weatherReason }};{{ dashboard?.mattressComfort?.firmnessReason }}
</p>
<div class="startup-scene-actions">
<button
class="startup-action-primary"
type="button"
:disabled="!dashboard?.tcmResult?.hasCompleted || !homeAssistant?.connected || sceneAvailable === 0 || !!sceneRunning"
@click="executeScene('sleep')"
>
{{ sceneRunning === 'sleep' ? '正在开启…' : '一键开启' }}
</button>
<button
class="startup-action-secondary"
type="button"
:disabled="!homeAssistant?.connected || sceneAvailable === 0 || !!sceneRunning"
@click="executeScene('all-off')"
>
{{ sceneRunning === 'all-off' ? '正在关闭…' : '一键关闭' }}
</button>
</div>
<div class="detail-entry-bar" @click="router.push('/device-control')">
<div class="entry-left">
<span class="icon">⚙️</span>
<div>
<strong>进入设备详情与独立参数调节</strong>
<small>显示每个设备在线状态,可单独开关与精细滑块调节</small>
</div>
</div>
<span class="arrow">&gt;</span>
</div>
</section>
<section class="metric-strip">
<div><small>睡眠目标</small><strong>7–8<em>h</em></strong></div>
<div><small>活动目标</small><strong>30<em>min</em></strong></div>
<div><small>方案版本</small><strong>V{{ dashboard?.currentPlan?.version || 0 }}</strong></div>
</section>
<p class="medical-note">健康建议仅用于日常管理演示,不构成医疗诊断或治疗意见。</p>
</template>
<!-- 固定底部导航栏 -->
<AppTabBar />
</main>
</template>