111
This commit is contained in:
+368
-29
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user