11
This commit is contained in:
+158
-31
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user