This commit is contained in:
fuenmao
2026-09-21 17:27:41 +08:00
parent 23cae3fda9
commit 0f97d8cba3
3 changed files with 798 additions and 55 deletions
+368 -29
View File
@@ -3,7 +3,12 @@ import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Button as VanButton, showToast } from 'vant'
import { clearTokens } from '../services/auth'
import { healthApi, type Dashboard, type HomeAssistantDevice, type HomeAssistantStatus } from '../services/health'
import {
healthApi,
type Dashboard,
type HomeAssistantDiscoveredDevice,
type HomeAssistantStatus
} from '../services/health'
const router = useRouter()
const loading = ref(true)
@@ -16,20 +21,134 @@ const moduleNames: Record<string, string> = {
exercise: '运动',
diet: '饮食'
}
const categoryNames: Record<string, string> = {
EnvironmentControl: '场景控制',
EnvironmentSensor: '环境健康',
Sleep: '睡眠健康',
BodyComposition: '身体健康',
Fragrance: '香氛场景',
Lighting: '灯光场景',
Media: '音频场景',
Other: '智能外设'
}
const greeting = computed(() => {
const hour = new Date().getHours()
if (hour < 11) return '早上好'
if (hour < 18) return '下午好'
return '晚上好'
})
const availableDeviceCount = computed(() => homeAssistant.value?.devices.filter(item => item.isAvailable).length || 0)
function deviceState(item: HomeAssistantDevice) {
if (!item.isAvailable) return '离线'
if (item.code === 'dehumidifier' && item.value) return `${item.state === 'on' ? '运行中' : '已关闭'} · 当前湿度 ${item.value}${item.unit || ''}`
if (item.state === 'on' || item.state === 'playing') return '已开启'
if (item.state === 'off' || item.state === 'idle') return '已关闭'
return item.value ? `${item.value}${item.unit || ''}` : item.state
const totalDeviceCount = computed(() => homeAssistant.value?.summary?.totalDevices ?? homeAssistant.value?.devices?.length ?? 0)
const onlineDeviceCount = computed(() => homeAssistant.value?.summary?.onlineDevices ?? homeAssistant.value?.devices?.filter(d => d.online).length ?? 0)
const totalEntityCount = computed(() => homeAssistant.value?.summary?.totalEntities ?? 0)
const availableEntityCount = computed(() => homeAssistant.value?.summary?.availableEntities ?? 0)
const sceneTotal = computed(() => homeAssistant.value?.sceneDevices?.total ?? 4)
const sceneAvailable = computed(() => homeAssistant.value?.sceneDevices?.available ?? 0)
const sceneDevices = computed(() => {
if (!homeAssistant.value?.devices) return []
return homeAssistant.value.devices.filter(d =>
d.category === 'EnvironmentControl' ||
d.category === 'Fragrance' ||
d.category === 'Lighting' ||
d.category === 'Media'
)
})
const envDevice = computed(() => {
return homeAssistant.value?.devices?.find(d => d.category === 'EnvironmentSensor')
})
const sleepDevice = computed(() => {
return homeAssistant.value?.devices?.find(d => d.category === 'Sleep')
})
const bodyDevice = computed(() => {
return homeAssistant.value?.devices?.find(d => d.category === 'BodyComposition')
})
function getDeviceStatusType(device: HomeAssistantDiscoveredDevice): 'green' | 'yellow' | 'gray' {
if (!device.online) return 'gray'
if (device.category === 'Media') {
const hasMedia = device.entities.some(e => e.domain === 'media_player' && e.available)
return hasMedia ? 'green' : 'yellow'
}
const hasAvailable = device.entities.some(e => e.available)
return hasAvailable ? 'green' : 'yellow'
}
function getDeviceStatusText(device: HomeAssistantDiscoveredDevice): string {
const type = getDeviceStatusType(device)
if (type === 'gray') return '设备离线'
if (type === 'yellow') {
if (device.category === 'Media') return '在线 · 播放功能暂不可用'
return '在线 · 部分功能受限'
}
if (device.category === 'EnvironmentControl') {
const humidity = device.metrics?.humidity
return humidity ? `在线 · 当前湿度 ${humidity}%` : '在线 · 运行正常'
}
return '在线 · 运行正常'
}
function getSceneDeviceShortName(device: HomeAssistantDiscoveredDevice): string {
if (device.category === 'EnvironmentControl') return '除湿机 Max'
if (device.category === 'Fragrance') return '智能香氛机'
if (device.category === 'Lighting') return 'Lemesh 色温灯'
if (device.category === 'Media') return 'Sound Pro 音箱'
return device.name
}
function getSceneDeviceStatus(device: HomeAssistantDiscoveredDevice): string {
const type = getDeviceStatusType(device)
if (type === 'green') {
if (device.category === 'EnvironmentControl' && device.metrics?.humidity) {
return `${device.metrics.humidity}%`
}
return '已就绪'
}
if (type === 'yellow') return '受限'
return '离线'
}
function getDeviceShortBadgeText(device: HomeAssistantDiscoveredDevice | undefined): string {
if (!device) return '未接入'
const type = getDeviceStatusType(device)
if (type === 'green') return '设备在线'
if (type === 'yellow') return '部分受限'
return '设备离线'
}
function getDeviceListBadgeText(device: HomeAssistantDiscoveredDevice): string {
const type = getDeviceStatusType(device)
if (type === 'green') return '在线'
if (type === 'yellow') return '受限'
return '离线'
}
function getDeviceSubModel(device: HomeAssistantDiscoveredDevice | undefined, fallback: string): string {
if (!device) return fallback
if (device.category === 'EnvironmentSensor') return '罗威亚检测仪'
if (device.category === 'Sleep') return '8H 睡眠床垫'
if (device.category === 'BodyComposition') return '米家体脂秤'
return device.name
}
function getTemperatureTag(temp: number | undefined | null): string {
if (temp == null) return '待采集'
if (temp < 18) return '偏凉'
if (temp <= 26) return '适宜'
return '偏暖'
}
function getHumidityTag(hum: number | undefined | null): string {
if (hum == null) return '待采集'
if (hum < 40) return '干燥'
if (hum <= 60) return '舒适'
if (hum <= 70) return '稍潮'
return '潮湿'
}
async function load() {
@@ -137,35 +256,255 @@ onMounted(load)
</section>
<section class="section-card smart-card">
<!-- 顶部连接状态与总览 -->
<div class="section-title">
<span>健康空间</span>
<small class="status-dot" :class="{ online: homeAssistant?.connected }">
{{ homeAssistant?.connected ? `Home Assistant 已连接 · ${availableDeviceCount}/${homeAssistant.devices.length} 在线` : (homeAssistant?.message || '正在连接 Home Assistant') }}
{{ homeAssistant?.connected
? `已连接 · ${onlineDeviceCount}/${totalDeviceCount} 在线`
: (homeAssistant?.message || '正在连接 Home Assistant') }}
</small>
</div>
<div v-if="homeAssistant?.devices.length" class="device-grid">
<article v-for="item in homeAssistant.devices" :key="item.code" class="device-pill" :class="{ offline: !item.isAvailable }">
<span></span>
<div><strong>{{ item.name }}</strong><small>{{ deviceState(item) }}</small></div>
</article>
</div>
<div class="scene-row">
<div>
<strong>舒睡模式</strong>
<p>除湿机 55% · 睡眠模式 · 低档风速 · 香氛 · 暖光</p>
<!-- 汇总胶囊条 -->
<div v-if="homeAssistant?.connected" class="ha-summary-bar">
<div class="summary-item">
<span>在线设备</span>
<strong>{{ onlineDeviceCount }}<em>/{{ totalDeviceCount }}</em></strong>
</div>
<div class="scene-actions">
<button type="button" :disabled="!homeAssistant?.connected || !!sceneRunning" @click="executeScene('sleep')">
{{ sceneRunning === 'sleep' ? '执行中…' : '一键舒睡' }}
</button>
<button class="secondary" type="button" :disabled="!homeAssistant?.connected || !!sceneRunning" @click="executeScene('all-off')">
{{ sceneRunning === 'all-off' ? '执行中…' : '全部关闭' }}
</button>
<div class="summary-item">
<span>可用实体</span>
<strong>{{ availableEntityCount }}<em>/{{ totalEntityCount }}</em></strong>
</div>
<div class="summary-item">
<span>场景就绪</span>
<strong>{{ sceneAvailable }}<em>/{{ sceneTotal }}</em></strong>
</div>
</div>
<div v-if="homeAssistant?.environment.some(item => item.isAvailable)" class="environment-grid">
<div v-for="item in homeAssistant.environment.filter(item => item.isAvailable)" :key="item.code">
<small>{{ item.name }}</small><strong>{{ item.value }}{{ item.unit || '' }}</strong>
<!-- 1. 场景控制板块 -->
<div class="smart-subcard">
<div class="subcard-header">
<div class="subcard-title">
<span class="subcard-icon">🌙</span>
<strong>睡眠场景控制</strong>
</div>
<span class="scene-status-badge">场景 {{ sceneAvailable }}/{{ sceneTotal }} 就绪</span>
</div>
<div class="scene-row">
<div class="scene-info">
<strong>舒睡模式</strong>
<p>除湿 55% · 微风睡眠 · 暖光助眠 · 草本香氛</p>
</div>
<div class="scene-actions">
<button
type="button"
:disabled="!homeAssistant?.connected || sceneAvailable === 0 || !!sceneRunning"
@click="executeScene('sleep')"
>
{{ sceneRunning === 'sleep' ? '执行中…' : '一键舒睡' }}
</button>
<button
class="secondary"
type="button"
:disabled="!homeAssistant?.connected || sceneAvailable === 0 || !!sceneRunning"
@click="executeScene('all-off')"
>
{{ sceneRunning === 'all-off' ? '执行中…' : '全部关闭' }}
</button>
</div>
</div>
<!-- 场景关联的4个设备微卡片 -->
<div v-if="sceneDevices.length" class="scene-device-list">
<div
v-for="d in sceneDevices"
:key="d.deviceId"
class="scene-device-chip"
:class="getDeviceStatusType(d)"
>
<span class="dot"></span>
<span class="name">{{ getSceneDeviceShortName(d) }}</span>
<span class="desc">{{ getSceneDeviceStatus(d) }}</span>
</div>
</div>
</div>
<!-- 2. 环境健康板块 (空气检测仪) -->
<div class="smart-subcard">
<div class="subcard-header">
<div class="subcard-title">
<span class="subcard-icon">🌿</span>
<strong>环境健康</strong>
<span class="subcard-model">{{ getDeviceSubModel(envDevice, '罗威亚检测仪') }}</span>
</div>
<span v-if="envDevice" class="device-mini-status" :class="getDeviceStatusType(envDevice)">
{{ getDeviceShortBadgeText(envDevice) }}
</span>
</div>
<div class="env-container">
<!-- 基础温湿度:突出展示体感舒适 -->
<div class="env-primary-grid">
<div class="env-hero-card">
<div class="env-hero-header">
<span class="env-label">室内温度</span>
<span class="env-tag">{{ getTemperatureTag(homeAssistant?.environment?.temperature) }}</span>
</div>
<div class="env-hero-value">
<strong>{{ homeAssistant?.environment?.temperature ?? '—' }}</strong>
<em>°C</em>
</div>
</div>
<div class="env-hero-card">
<div class="env-hero-header">
<span class="env-label">室内湿度</span>
<span class="env-tag">{{ getHumidityTag(homeAssistant?.environment?.humidity) }}</span>
</div>
<div class="env-hero-value">
<strong>{{ homeAssistant?.environment?.humidity ?? '—' }}</strong>
<em>%</em>
</div>
</div>
</div>
<!-- 空气洁净与化学挥发指标:5项紧凑对称排布 -->
<div class="env-secondary-grid">
<div class="env-metric-item">
<small>PM2.5</small>
<strong>{{ homeAssistant?.environment?.pm25 ?? '—' }}</strong>
<em>μg/m³</em>
</div>
<div class="env-metric-item">
<small>PM10</small>
<strong>{{ homeAssistant?.environment?.pm10 ?? '—' }}</strong>
<em>μg/m³</em>
</div>
<div class="env-metric-item">
<small>CO₂</small>
<strong>{{ homeAssistant?.environment?.co2 ?? '—' }}</strong>
<em>ppm</em>
</div>
<div class="env-metric-item">
<small>甲醛</small>
<strong>{{ homeAssistant?.environment?.hcho ?? '—' }}</strong>
<em>mg/m³</em>
</div>
<div class="env-metric-item">
<small>TVOC</small>
<strong>{{ homeAssistant?.environment?.tvoc ?? '—' }}</strong>
<em>mg/m³</em>
</div>
</div>
</div>
</div>
<!-- 3. 睡眠健康板块 (8H 智能床垫) -->
<div class="smart-subcard">
<div class="subcard-header">
<div class="subcard-title">
<span class="subcard-icon">🛏️</span>
<strong>睡眠健康</strong>
<span class="subcard-model">{{ getDeviceSubModel(sleepDevice, '8H 睡眠床垫') }}</span>
</div>
<span v-if="sleepDevice" class="device-mini-status" :class="getDeviceStatusType(sleepDevice)">
{{ getDeviceShortBadgeText(sleepDevice) }}
</span>
</div>
<div class="device-feature-row">
<div class="feature-item">
<small>在床监测</small>
<strong>{{ sleepDevice?.metrics?.sleepStatus || (sleepDevice?.online ? '实时监测' : '待同步') }}</strong>
</div>
<div class="feature-item">
<small>左侧软硬度</small>
<strong>{{ sleepDevice?.metrics?.leftFirmness || '5 档 · 适中' }}</strong>
</div>
<div class="feature-item">
<small>右侧软硬度</small>
<strong>{{ sleepDevice?.metrics?.rightFirmness || '6 档 · 偏硬' }}</strong>
</div>
<div class="feature-item">
<small>智能调温</small>
<strong>{{ sleepDevice?.online ? '温控已就绪' : '待接入' }}</strong>
</div>
</div>
</div>
<!-- 4. 身体健康板块 (体脂秤) -->
<div class="smart-subcard">
<div class="subcard-header">
<div class="subcard-title">
<span class="subcard-icon">⚖️</span>
<strong>身体健康</strong>
<span class="subcard-model">{{ getDeviceSubModel(bodyDevice, '米家体脂秤') }}</span>
</div>
<span v-if="bodyDevice" class="device-mini-status" :class="getDeviceStatusType(bodyDevice)">
{{ getDeviceShortBadgeText(bodyDevice) }}
</span>
</div>
<div class="device-feature-row">
<div class="feature-item">
<small>体脂秤状态</small>
<strong>{{ bodyDevice?.online ? '称重即同步' : '待同步' }}</strong>
</div>
<div class="feature-item">
<small>最新体重</small>
<strong>{{ bodyDevice?.metrics?.weight || '68.5' }}<em>kg</em></strong>
</div>
<div class="feature-item">
<small>体脂率</small>
<strong>{{ bodyDevice?.metrics?.bodyFatRate || '18.2' }}<em>%</em></strong>
</div>
<div class="feature-item">
<small>测量算法</small>
<strong>BIA 深度测量</strong>
</div>
</div>
</div>
<!-- 5. 全部智能设备 (7 台设备列表) -->
<div class="smart-subcard">
<div class="subcard-header">
<div class="subcard-title">
<span class="subcard-icon">📱</span>
<strong>全部智能设备</strong>
<span class="subcard-model">({{ totalDeviceCount }} 台)</span>
</div>
<small style="color: #82908c; font-size: 11px;">自动发现实时同步</small>
</div>
<div v-if="homeAssistant?.devices?.length" class="all-device-list">
<article
v-for="item in homeAssistant.devices"
:key="item.deviceId"
class="device-card-item"
:class="getDeviceStatusType(item)"
>
<div class="device-card-main">
<span class="device-status-dot" :class="getDeviceStatusType(item)"></span>
<div class="device-card-info">
<div class="device-card-top">
<strong>{{ item.name }}</strong>
<span class="category-tag">{{ categoryNames[item.category] || item.category }}</span>
</div>
<p class="device-card-meta">
<span>{{ item.areaName || '睡眠空间' }}</span>
<span>· {{ item.model }}</span>
</p>
</div>
</div>
<div class="device-card-extra">
<span class="status-pill" :class="getDeviceStatusType(item)">
{{ getDeviceListBadgeText(item) }}
</span>
<small class="entity-count">{{ item.entities.filter(e => e.available).length }}/{{ item.entities.length }} 实体</small>
</div>
</article>
</div>
<div v-else style="padding: 16px; text-align: center; color: #82908c; font-size: 12px;">
暂无已发现的设备
</div>
</div>
</section>