196 lines
7.8 KiB
Vue
196 lines
7.8 KiB
Vue
<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">去评估 ></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">></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>
|