11
This commit is contained in:
+35
-565
@@ -7,33 +7,14 @@ import { clearTokens } from '../services/auth'
|
||||
import {
|
||||
healthApi,
|
||||
type Dashboard,
|
||||
type HomeAssistantDiscoveredDevice,
|
||||
type HomeAssistantStatus
|
||||
} from '../services/health'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(true)
|
||||
const generating = ref(false)
|
||||
const sceneRunning = ref<'sleep' | 'all-off'>()
|
||||
const dashboard = ref<Dashboard>()
|
||||
const homeAssistant = ref<HomeAssistantStatus>()
|
||||
const moduleNames: Record<string, string> = {
|
||||
sleep: '睡眠',
|
||||
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 '早上好'
|
||||
@@ -41,176 +22,8 @@ const greeting = computed(() => {
|
||||
return '晚上好'
|
||||
})
|
||||
|
||||
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 ?? 0)
|
||||
const sceneAvailable = computed(() => homeAssistant.value?.sceneDevices?.available ?? 0)
|
||||
|
||||
const sceneDevices = computed(() => {
|
||||
if (!homeAssistant.value?.devices) return []
|
||||
const configuredCategories = new Set(
|
||||
(homeAssistant.value.scenePresets || []).map(item => item.category)
|
||||
)
|
||||
return homeAssistant.value.devices.filter(d => configuredCategories.has(d.category))
|
||||
})
|
||||
|
||||
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 '潮湿'
|
||||
}
|
||||
|
||||
const switchingDeviceId = ref<string | null>(null)
|
||||
const syncingSensor = ref(false)
|
||||
const showScenePresets = ref(true)
|
||||
|
||||
function isControllable(device: HomeAssistantDiscoveredDevice): boolean {
|
||||
return !!device.primaryControlEntityId
|
||||
}
|
||||
|
||||
function getPrimaryControlEntity(device: HomeAssistantDiscoveredDevice) {
|
||||
if (!device.primaryControlEntityId) return undefined
|
||||
return device.entities.find(e => e.entityId === device.primaryControlEntityId)
|
||||
}
|
||||
|
||||
function isPrimaryControlAvailable(device: HomeAssistantDiscoveredDevice): boolean {
|
||||
return !!getPrimaryControlEntity(device)?.available
|
||||
}
|
||||
|
||||
function isDeviceTurnedOn(device: HomeAssistantDiscoveredDevice): boolean {
|
||||
const target = getPrimaryControlEntity(device)
|
||||
if (!target) return false
|
||||
return target.state === 'on' || target.state === 'playing'
|
||||
}
|
||||
|
||||
async function toggleDeviceSwitch(device: HomeAssistantDiscoveredDevice) {
|
||||
if (!isPrimaryControlAvailable(device)) {
|
||||
showToast('设备主控功能当前不可用')
|
||||
return
|
||||
}
|
||||
const currentOn = isDeviceTurnedOn(device)
|
||||
const nextOn = !currentOn
|
||||
switchingDeviceId.value = device.deviceId
|
||||
try {
|
||||
const res = await healthApi.switchDevice({
|
||||
deviceId: device.deviceId,
|
||||
turnOn: nextOn,
|
||||
entityId: device.primaryControlEntityId || undefined
|
||||
})
|
||||
showToast({
|
||||
type: res.success ? 'success' : 'fail',
|
||||
message: res.message || (nextOn ? '已开启设备' : '已关闭设备')
|
||||
})
|
||||
homeAssistant.value = await healthApi.homeAssistantStatus()
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : '控制失败')
|
||||
} finally {
|
||||
switchingDeviceId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function syncSensorData() {
|
||||
syncingSensor.value = true
|
||||
try {
|
||||
homeAssistant.value = await healthApi.homeAssistantStatus()
|
||||
showToast({ type: 'success', message: '状态已刷新' })
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : '刷新失败')
|
||||
} finally {
|
||||
syncingSensor.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -242,19 +55,6 @@ async function executeScene(scene: 'sleep' | 'all-off') {
|
||||
}
|
||||
}
|
||||
|
||||
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 logout() {
|
||||
clearTokens()
|
||||
router.replace('/auth')
|
||||
@@ -279,6 +79,21 @@ onMounted(load)
|
||||
<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.push('/assessment')"
|
||||
>
|
||||
去健康评估
|
||||
</van-button>
|
||||
</section>
|
||||
|
||||
<!-- 未完成评估提示横幅 -->
|
||||
<section v-if="!dashboard?.hasCompletedAssessment" class="assessment-guide-banner" @click="router.push('/assessment')">
|
||||
<div class="guide-badge">重要提醒</div>
|
||||
@@ -295,9 +110,8 @@ onMounted(load)
|
||||
<section v-else class="section-card personalized-startup-card">
|
||||
<div class="section-title">
|
||||
<span>专属舒睡启动参数</span>
|
||||
<span class="customized-tag">根据身体评估专属定制</span>
|
||||
</div>
|
||||
<p class="startup-card-desc">依据您的冷热偏好、入睡状态与骨骼肌肉承托需求,智能中枢已计算出全套设备预设:</p>
|
||||
<p class="startup-card-desc">依据您的体征评估与冷热、承托偏好,智能中枢已生成专属预设:</p>
|
||||
|
||||
<div class="startup-params-tags">
|
||||
<div class="startup-param-badge">
|
||||
@@ -322,6 +136,25 @@ onMounted(load)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="startup-scene-actions">
|
||||
<button
|
||||
class="startup-action-primary"
|
||||
type="button"
|
||||
:disabled="!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>
|
||||
@@ -334,375 +167,12 @@ onMounted(load)
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="hero-card">
|
||||
<p class="eyebrow">今日健康建议</p>
|
||||
<h2>{{ dashboard?.currentPlan?.title || '尚未生成健康方案' }}</h2>
|
||||
<p>{{ dashboard?.currentPlan?.summary || '完成健康板块选择后,即可生成第一版个人参数。' }}</p>
|
||||
<div class="tag-row">
|
||||
<span v-for="code in dashboard?.selectedModules" :key="code">{{ moduleNames[code] || code }}</span>
|
||||
</div>
|
||||
<van-button
|
||||
v-if="!dashboard?.currentPlan"
|
||||
round
|
||||
color="#f1c37a"
|
||||
text-color="#183b34"
|
||||
:loading="generating"
|
||||
@click="generate"
|
||||
>
|
||||
立即生成
|
||||
</van-button>
|
||||
</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>
|
||||
|
||||
<section class="section-card">
|
||||
<div class="section-title"><span>我的健康板块</span><button type="button" @click="router.push('/onboarding')">调整</button></div>
|
||||
<article v-for="item in dashboard?.currentPlan?.items" :key="item.moduleCode" class="plan-item">
|
||||
<div class="plan-index">{{ item.moduleCode === 'sleep' ? '☾' : item.moduleCode === 'exercise' ? '↗' : '◒' }}</div>
|
||||
<div>
|
||||
<div class="plan-heading"><strong>{{ item.title }}</strong><span>{{ item.target }}</span></div>
|
||||
<p>{{ item.advice }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="section-card smart-card">
|
||||
<!-- 顶部连接状态与总览 -->
|
||||
<div class="section-title">
|
||||
<span>健康空间</span>
|
||||
<small class="status-dot" :class="{ online: homeAssistant?.connected }">
|
||||
{{ homeAssistant?.connected
|
||||
? `已连接 · ${onlineDeviceCount}/${totalDeviceCount} 在线`
|
||||
: (homeAssistant?.message || '正在连接 Home Assistant') }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- 汇总胶囊条 -->
|
||||
<div v-if="homeAssistant?.connected" class="ha-summary-bar">
|
||||
<div class="summary-item">
|
||||
<span>在线设备</span>
|
||||
<strong>{{ onlineDeviceCount }}<em>/{{ totalDeviceCount }}</em></strong>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- 1. 睡眠场景控制 -->
|
||||
<div class="smart-subcard">
|
||||
<div class="subcard-header">
|
||||
<div class="subcard-title">
|
||||
<span class="subcard-icon">🌙</span>
|
||||
<strong>睡眠场景控制</strong>
|
||||
<span class="subcard-model">({{ sceneAvailable }}/{{ sceneTotal }} 项可用)</span>
|
||||
</div>
|
||||
<button
|
||||
class="preset-toggle-btn"
|
||||
type="button"
|
||||
@click="showScenePresets = !showScenePresets"
|
||||
>
|
||||
{{ showScenePresets ? '收起参数 ▲' : '控制参数详情 ▼' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="scene-row">
|
||||
<div class="scene-info">
|
||||
<strong>一键舒睡</strong>
|
||||
<p>仅执行已配置且当前可用的恒湿、香氛与暖光动作</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>
|
||||
|
||||
<!-- 场景设备实时可用状态 -->
|
||||
<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="device-mini-status" :class="getDeviceStatusType(d)">
|
||||
{{ getSceneDeviceStatus(d) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 后台真实配置的场景动作 -->
|
||||
<div v-if="showScenePresets && homeAssistant?.scenePresets?.length" class="scene-presets-panel">
|
||||
<div class="preset-panel-header">
|
||||
<span>场景执行参数</span>
|
||||
<small>仅展示后台实际下发的动作</small>
|
||||
</div>
|
||||
<div class="preset-items-grid">
|
||||
<div
|
||||
v-for="item in homeAssistant.scenePresets"
|
||||
:key="item.deviceName"
|
||||
class="preset-card-item"
|
||||
:class="{ unavailable: !item.available }"
|
||||
>
|
||||
<div class="preset-card-top">
|
||||
<span class="preset-icon">
|
||||
{{ item.category === 'EnvironmentControl' ? '💧' : item.category === 'Fragrance' ? '🌸' : item.category === 'Lighting' ? '💡' : item.category === 'Media' ? '🔊' : item.category === 'Sleep' ? '🛏️' : item.category === 'EnvironmentSensor' ? '🌿' : '⚖️' }}
|
||||
</span>
|
||||
<strong class="preset-name">{{ item.deviceName }}</strong>
|
||||
<span class="preset-action">{{ item.action }}</span>
|
||||
</div>
|
||||
<p class="preset-detail">{{ item.detail }}</p>
|
||||
<p class="preset-availability" :class="{ unavailable: !item.available }">
|
||||
{{ item.availabilityMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</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>
|
||||
<button
|
||||
class="sensor-mini-sync-btn"
|
||||
:disabled="syncingSensor"
|
||||
type="button"
|
||||
@click="syncSensorData"
|
||||
>
|
||||
{{ syncingSensor ? '刷新中…' : '立即刷新' }}
|
||||
</button>
|
||||
</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 || '暂无数据' }}</strong>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<small>右侧软硬度</small>
|
||||
<strong>{{ sleepDevice?.metrics?.rightFirmness || '暂无数据' }}</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>
|
||||
<button
|
||||
class="sensor-mini-sync-btn"
|
||||
:disabled="syncingSensor"
|
||||
type="button"
|
||||
@click="syncSensorData"
|
||||
>
|
||||
{{ syncingSensor ? '刷新中…' : '刷新状态' }}
|
||||
</button>
|
||||
</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 ?? '—' }}<em v-if="bodyDevice?.metrics?.weight != null">kg</em></strong>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<small>体脂率</small>
|
||||
<strong>{{ bodyDevice?.metrics?.bodyFatRate ?? '—' }}<em v-if="bodyDevice?.metrics?.bodyFatRate != null">%</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">
|
||||
<button
|
||||
v-if="isControllable(item)"
|
||||
class="device-switch-pill"
|
||||
:class="{ on: isDeviceTurnedOn(item), disabled: !isPrimaryControlAvailable(item) }"
|
||||
:disabled="!isPrimaryControlAvailable(item) || switchingDeviceId === item.deviceId"
|
||||
type="button"
|
||||
@click="toggleDeviceSwitch(item)"
|
||||
>
|
||||
<span class="switch-knob"></span>
|
||||
<span class="switch-text">{{ switchingDeviceId === item.deviceId ? '…' : (isDeviceTurnedOn(item) ? '已开启' : '已关闭') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="sensor-sync-pill"
|
||||
:disabled="syncingSensor"
|
||||
type="button"
|
||||
@click="syncSensorData"
|
||||
>
|
||||
{{ syncingSensor ? '同步中' : '刷新' }}
|
||||
</button>
|
||||
<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 class="all-control-banner" @click="router.push('/device-control')">
|
||||
<div class="ac-text">
|
||||
<span class="ac-icon">🎛️</span>
|
||||
<div>
|
||||
<strong>查看全部 {{ totalDeviceCount }} 台设备状态与独立参数控制</strong>
|
||||
<small>支持湿度、色温、左右加热档位与承托模式单独调节</small>
|
||||
</div>
|
||||
</div>
|
||||
<span class="ac-arrow">></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="medical-note">健康建议仅用于日常管理演示,不构成医疗诊断或治疗意见。</p>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user