This commit is contained in:
fuenmao
2026-09-22 14:16:58 +08:00
parent 0f97d8cba3
commit 426eaca97f
3 changed files with 381 additions and 41 deletions
+158 -31
View File
@@ -44,17 +44,15 @@ const totalDeviceCount = computed(() => homeAssistant.value?.summary?.totalDevic
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 sceneTotal = computed(() => homeAssistant.value?.sceneDevices?.total ?? 0)
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 configuredCategories = new Set(
(homeAssistant.value.scenePresets || []).map(item => item.category)
)
return homeAssistant.value.devices.filter(d => configuredCategories.has(d.category))
})
const envDevice = computed(() => {
@@ -151,6 +149,67 @@ function getHumidityTag(hum: number | undefined | null): string {
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 {
@@ -282,20 +341,27 @@ onMounted(load)
</div>
</div>
<!-- 1. 场景控制板块 -->
<!-- 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>
<span class="scene-status-badge">场景 {{ sceneAvailable }}/{{ sceneTotal }} 就绪</span>
<button
class="preset-toggle-btn"
type="button"
@click="showScenePresets = !showScenePresets"
>
{{ showScenePresets ? '收起参数 ▲' : '控制参数详情 ▼' }}
</button>
</div>
<div class="scene-row">
<div class="scene-info">
<strong>舒睡模式</strong>
<p>除湿 55% · 微风睡眠 · 暖光助眠 · 草本香氛</p>
<strong>一键舒睡</strong>
<p>仅执行已配置且当前可用的恒湿、香氛与暖光动作</p>
</div>
<div class="scene-actions">
<button
@@ -311,12 +377,12 @@ onMounted(load)
:disabled="!homeAssistant?.connected || sceneAvailable === 0 || !!sceneRunning"
@click="executeScene('all-off')"
>
{{ sceneRunning === 'all-off' ? '执行中…' : '全部关闭' }}
{{ sceneRunning === 'all-off' ? '执行中…' : '关闭场景设备' }}
</button>
</div>
</div>
<!-- 场景关联的4个设备微卡片 -->
<!-- 场景设备实时可用状态 -->
<div v-if="sceneDevices.length" class="scene-device-list">
<div
v-for="d in sceneDevices"
@@ -326,7 +392,37 @@ onMounted(load)
>
<span class="dot"></span>
<span class="name">{{ getSceneDeviceShortName(d) }}</span>
<span class="desc">{{ getSceneDeviceStatus(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>
@@ -339,9 +435,14 @@ onMounted(load)
<strong>环境健康</strong>
<span class="subcard-model">{{ getDeviceSubModel(envDevice, '罗威亚检测仪') }}</span>
</div>
<span v-if="envDevice" class="device-mini-status" :class="getDeviceStatusType(envDevice)">
{{ getDeviceShortBadgeText(envDevice) }}
</span>
<button
class="sensor-mini-sync-btn"
:disabled="syncingSensor"
type="button"
@click="syncSensorData"
>
{{ syncingSensor ? '刷新中…' : '立即刷新' }}
</button>
</div>
<div class="env-container">
@@ -408,7 +509,11 @@ onMounted(load)
<strong>睡眠健康</strong>
<span class="subcard-model">{{ getDeviceSubModel(sleepDevice, '8H 睡眠床垫') }}</span>
</div>
<span v-if="sleepDevice" class="device-mini-status" :class="getDeviceStatusType(sleepDevice)">
<span
v-if="sleepDevice"
class="device-mini-status"
:class="getDeviceStatusType(sleepDevice)"
>
{{ getDeviceShortBadgeText(sleepDevice) }}
</span>
</div>
@@ -419,15 +524,15 @@ onMounted(load)
</div>
<div class="feature-item">
<small>左侧软硬度</small>
<strong>{{ sleepDevice?.metrics?.leftFirmness || '5 档 · 适中' }}</strong>
<strong>{{ sleepDevice?.metrics?.leftFirmness || '暂无数据' }}</strong>
</div>
<div class="feature-item">
<small>右侧软硬度</small>
<strong>{{ sleepDevice?.metrics?.rightFirmness || '6 档 · 偏硬' }}</strong>
<strong>{{ sleepDevice?.metrics?.rightFirmness || '暂无数据' }}</strong>
</div>
<div class="feature-item">
<small>智能调温</small>
<strong>{{ sleepDevice?.online ? '温控已就绪' : '待接入' }}</strong>
<strong>{{ sleepDevice?.online ? '状态待读取' : '待接入' }}</strong>
</div>
</div>
</div>
@@ -440,22 +545,27 @@ onMounted(load)
<strong>身体健康</strong>
<span class="subcard-model">{{ getDeviceSubModel(bodyDevice, '米家体脂秤') }}</span>
</div>
<span v-if="bodyDevice" class="device-mini-status" :class="getDeviceStatusType(bodyDevice)">
{{ getDeviceShortBadgeText(bodyDevice) }}
</span>
<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>
<strong>{{ bodyDevice?.online ? '设备在线' : '待同步' }}</strong>
</div>
<div class="feature-item">
<small>最新体重</small>
<strong>{{ bodyDevice?.metrics?.weight || '68.5' }}<em>kg</em></strong>
<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 || '18.2' }}<em>%</em></strong>
<strong>{{ bodyDevice?.metrics?.bodyFatRate ?? '—' }}<em v-if="bodyDevice?.metrics?.bodyFatRate != null">%</em></strong>
</div>
<div class="feature-item">
<small>测量算法</small>
@@ -472,7 +582,7 @@ onMounted(load)
<strong>全部智能设备</strong>
<span class="subcard-model">({{ totalDeviceCount }} 台)</span>
</div>
<small style="color: #82908c; font-size: 11px;">自动发现实时同步</small>
<small style="color: #82908c; font-size: 11px;">已确认主控能力的设备可操作</small>
</div>
<div v-if="homeAssistant?.devices?.length" class="all-device-list">
@@ -490,15 +600,32 @@ onMounted(load)
<span class="category-tag">{{ categoryNames[item.category] || item.category }}</span>
</div>
<p class="device-card-meta">
<span>{{ item.areaName || '睡眠空间' }}</span>
<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>
<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>