This commit is contained in:
fuenmao
2026-10-09 18:06:21 +08:00
parent 563836f789
commit 20467ca479
18 changed files with 547 additions and 102 deletions
+2 -1
View File
@@ -2,6 +2,7 @@
import { computed, ref } from 'vue'
import type { HomeAssistantDiscoveredDevice } from '../../services/health'
import { useDeviceControlContext } from '../../devices/controlContext'
import { entityStateLabel } from '../../devices/entityState'
const props = defineProps<{ device: HomeAssistantDiscoveredDevice }>()
const { operationBusy, toggleDevice, isDeviceTurnedOn, isDeviceControlAvailable, switchingDeviceId } = useDeviceControlContext()
const expanded = ref(false)
@@ -25,7 +26,7 @@ const entities = computed(() => expanded.value ? props.device.entities : props.d
<div class="params-body">
<div v-for="entity in entities" :key="entity.entityId" class="param-row">
<span class="p-title">{{ entity.name }}</span>
<span class="p-val">{{ entity.available && !['unknown', 'unavailable'].includes(entity.state) ? entity.state : '不可用' }} {{ entity.unit || '' }}</span>
<span class="p-val">{{ entityStateLabel(entity) }} {{ entity.unit || '' }}</span>
</div>
<button v-if="device.entities.length > 6" class="text-refresh" @click="expanded = !expanded">
{{ expanded ? '收起能力' : '查看全部能力' }}
+22 -84
View File
@@ -1,94 +1,32 @@
<script setup lang="ts">
import { toRefs } from 'vue'
import { Button as VanButton, Slider as VanSlider } from 'vant'
import { computed } from 'vue'
import type { HomeAssistantDiscoveredDevice } from '../../services/health'
import { useDeviceControlContext } from '../../devices/controlContext'
import { useDeviceCardState } from '../../devices/useDeviceCardState'
import { lightCapabilities } from '../../devices/lighting/capabilities'
import StandardLightCard from './StandardLightCard.vue'
import { lightTransitionForDevice } from '../../devices/lighting/transitions'
const props = defineProps<{ device: HomeAssistantDiscoveredDevice }>()
const { device: lightDevice } = toRefs(props)
const { localParams, operationBusy, switchingDeviceId, isDeviceTurnedOn, toggleDevice, applyParam, controlLightTransition, lightTransition, loading } = useDeviceControlContext()
const state = useDeviceCardState(() => props.device)
const { operationBusy, lightTransition, controlLightTransition } = useDeviceControlContext()
const transition = computed(() => lightTransitionForDevice(props.device, lightTransition.value))
</script>
<template>
<section class="device-card" v-if="lightDevice">
<div class="device-head">
<div class="dev-info">
<span class="category-tag">卧室灯光</span>
<h3>{{ lightDevice.name }}</h3>
<span class="status-dot" :class="{ online: lightDevice.online }">
{{ lightDevice.online ? '设备在线' : '设备离线' }}
</span>
</div>
<VanButton
size="small"
round
:type="isDeviceTurnedOn(lightDevice) ? 'primary' : 'default'"
:color="isDeviceTurnedOn(lightDevice) ? '#0c5045' : ''"
:loading="switchingDeviceId === lightDevice.deviceId"
:disabled="operationBusy || !state.powerAvailable()"
@click="toggleDevice(lightDevice)"
>
{{ state.powerLabel('已点亮', '已熄灭') }}
</VanButton>
<StandardLightCard :device="device" category-label="卧室灯光"
:reported-brightness="transition?.running ? transition.brightnessPercent : undefined"
note="手动调节会停止当前渐变;本灯保留助眠、唤醒及灯灭停止按摩的专属联动。">
<template #before-controls>
<div class="light-mode-panel">
<div>
<strong v-if="transition?.running">{{ transition.mode === 'wake' ? '晨起唤醒中' : '助眠渐暗中' }} · {{ transition.brightnessPercent }}%</strong>
<strong v-else>渐变灯光</strong>
<small>{{ transition?.message || '开灯从50%逐渐变暗;晨起从当前亮度逐渐变亮至70%' }}</small>
</div>
<div class="params-body">
<div class="light-mode-panel">
<div>
<strong v-if="lightTransition?.running">
{{ lightTransition.mode === 'wake' ? '晨起唤醒中' : '助眠渐暗中' }} · {{ lightTransition.brightnessPercent }}%
</strong>
<strong v-else>渐变灯光</strong>
<small>{{ lightTransition?.message || '开灯从50%逐渐变暗;晨起从当前亮度逐渐变亮至70%' }}</small>
</div>
<div class="light-mode-actions">
<button type="button" :disabled="operationBusy || !state.available('brightness')" @click="controlLightTransition('sleep')">🌙 开始助眠渐暗</button>
<button type="button" :disabled="operationBusy || !state.available('brightness')" @click="controlLightTransition('wake')">🌅 晨起唤醒</button>
<button v-if="lightTransition?.running" type="button" :disabled="operationBusy" @click="controlLightTransition('stop')">停止渐变</button>
</div>
</div>
<!-- 亮度滑块 -->
<div class="param-row">
<div class="param-header">
<span class="p-title">灯光亮度</span>
<strong class="p-val">{{ lightTransition?.running ? `${localParams.lightBrightness}%` : state.display('brightness', localParams.lightBrightness, '%') }}</strong>
</div>
<VanSlider
v-model="localParams.lightBrightness"
:min="0"
:max="100"
:disabled="operationBusy || !state.available('brightness')"
active-color="#d6a657"
@change="applyParam(lightDevice.deviceId, 'brightness', localParams.lightBrightness)"
/>
<div class="slider-scale">
<span>0% 关闭灯光</span>
<span>50% 舒缓</span>
<span>100% 满亮</span>
</div>
</div>
<!-- 色温滑块 -->
<div class="param-row">
<div class="param-header">
<span class="p-title">灯光色温调节</span>
<strong class="p-val">{{ state.display('color_temp', localParams.lightColorTemp, 'K') }}</strong>
</div>
<VanSlider
v-model="localParams.lightColorTemp"
:min="2200"
:max="5000"
:step="50"
:disabled="operationBusy || !state.available('color_temp')"
active-color="#c87f2b"
@change="applyParam(lightDevice.deviceId, 'color_temp', localParams.lightColorTemp)"
/>
<div class="slider-scale">
<span>2200K 超暖橙光 (利于褪黑素)</span>
<span>5000K 清爽白光</span>
</div>
</div>
<div class="light-mode-actions">
<button type="button" :disabled="operationBusy || !lightCapabilities(device).available || !lightCapabilities(device).canDim" @click="controlLightTransition('sleep')">🌙 开始助眠渐暗</button>
<button type="button" :disabled="operationBusy || !lightCapabilities(device).available || !lightCapabilities(device).canDim" @click="controlLightTransition('wake')">🌅 晨起唤醒</button>
<button v-if="transition?.running" type="button" :disabled="operationBusy" @click="controlLightTransition('stop')">停止渐变</button>
</div>
</section>
</div>
</template>
</StandardLightCard>
</template>
@@ -0,0 +1,40 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Button as VanButton } from 'vant'
import type { HomeAssistantDiscoveredDevice } from '../../services/health'
import { useDeviceControlContext } from '../../devices/controlContext'
import { lightCapabilities } from '../../devices/lighting/capabilities'
import StandardLightControls from './StandardLightControls.vue'
import { lightTransitionForDevice } from '../../devices/lighting/transitions'
const props = withDefaults(defineProps<{ device: HomeAssistantDiscoveredDevice; categoryLabel?: string; note?: string; reportedBrightness?: number }>(),
{ categoryLabel: '灯光控制' })
const { operationBusy, switchingDeviceId, toggleDevice, lightTransition } = useDeviceControlContext()
const capability = computed(() => lightCapabilities(props.device))
const transition = computed(() => lightTransitionForDevice(props.device, lightTransition.value))
const brightness = computed(() => props.reportedBrightness ?? (transition.value?.running ? transition.value.brightnessPercent : undefined))
const note = computed(() => props.note ?? (props.device.defaultLightEffect
? '已加入一键舒睡和关闭;从50%开始,每5秒降低1%,最终关灯。手动调节仅停止本灯渐变。'
: '按本灯实际上报的能力显示参数;单独控制,不自动加入舒睡或唤醒场景。'))
</script>
<template>
<section class="device-card">
<div class="device-head">
<div class="dev-info"><span class="category-tag">{{ categoryLabel }}</span><h3>{{ device.name }}</h3>
<span class="status-dot" :class="{ online: capability.available }">{{ capability.available ? capability.entity?.state === 'on' ? '已点亮' : '已熄灭' : '灯光主控当前不可用' }}</span>
</div>
<VanButton size="small" round :type="capability.entity?.state === 'on' ? 'primary' : 'default'"
:loading="switchingDeviceId === device.deviceId" :disabled="operationBusy || !capability.available" @click="toggleDevice(device)">
{{ capability.available ? capability.entity?.state === 'on' ? '关闭' : '开启' : '暂不可用' }}
</VanButton>
</div>
<div class="params-body">
<slot name="before-controls" />
<p v-if="device.defaultLightEffect && (transition?.running || transition?.message)" class="scale-note"
:class="{ unavailable: transition?.success === false }">{{ transition?.running ? `助眠渐暗中 · ${transition.brightnessPercent}%` : transition?.message }}</p>
<StandardLightControls :device="device" :reported-brightness="brightness" />
<slot name="after-controls" />
<p class="scale-note">{{ note }}</p>
</div>
</section>
</template>
@@ -0,0 +1,42 @@
<script setup lang="ts">
import { Slider as VanSlider } from 'vant'
import type { HomeAssistantDiscoveredDevice } from '../../services/health'
import { useDeviceControlContext } from '../../devices/controlContext'
import { useLightControls } from '../../devices/lighting/useLightControls'
const props = defineProps<{ device: HomeAssistantDiscoveredDevice; reportedBrightness?: number }>()
const { operationBusy, applyParam } = useDeviceControlContext()
const { capability, brightnessReading, brightnessTarget, temperatureTarget, effectTarget, defaultEffect, setBrightness, setTemperature, setEffect } =
useLightControls(() => props.device, () => operationBusy.value, applyParam, () => props.reportedBrightness)
</script>
<template>
<div v-if="capability.canDim" class="param-row">
<div class="param-header"><span class="p-title">灯光亮度</span><strong class="p-val">{{ brightnessReading === undefined ? '--' : `${brightnessReading}%` }}</strong></div>
<VanSlider v-model="brightnessTarget" :min="0" :max="100" :step="1" :disabled="operationBusy || !capability.available"
active-color="#d6a657" aria-label="待设置的灯光亮度" @change="setBrightness" />
<div class="slider-scale"><span>0% 关闭</span><span>100% 满亮</span></div>
<p v-if="capability.available" class="scale-note">目标亮度 {{ brightnessTarget }}%;滑动后设置,非当前回读值。</p>
</div>
<div v-if="capability.temperatureRange" class="param-row">
<div class="param-header"><span class="p-title">灯光色温调节</span><strong class="p-val">{{ capability.colorTemperature === undefined ? '--' : `${capability.colorTemperature}K` }}</strong></div>
<VanSlider v-model="temperatureTarget" :min="capability.temperatureRange.min" :max="capability.temperatureRange.max" :step="1"
:disabled="operationBusy || !capability.available" active-color="#c87f2b" aria-label="待设置的灯光色温" @change="setTemperature" />
<div class="slider-scale"><span>{{ capability.temperatureRange.min }}K 暖光</span><span>{{ capability.temperatureRange.max }}K 冷光</span></div>
<p v-if="capability.available" class="scale-note">目标色温 {{ temperatureTarget }}K;滑动后设置,关灯时调节会点亮灯。</p>
</div>
<div v-if="capability.effects.length" class="param-row">
<div class="param-header"><span class="p-title">灯光模式</span><strong class="p-val">{{ capability.effect || '--' }}</strong></div>
<select v-model="effectTarget" class="standard-light-select" aria-label="选择灯光模式" :disabled="operationBusy || !capability.available" @change="setEffect">
<option value="" disabled>选择要设置的模式</option>
<option v-for="effect in capability.effects" :key="effect" :value="effect">{{ effect }}</option>
</select>
<p class="scale-note">使用这盏灯实际提供的模式;选择后可能点亮灯。</p>
<p v-if="defaultEffect" class="scale-note">舒睡默认 {{ defaultEffect }};实际模式以顶部回读为准。</p>
</div>
<p v-if="!capability.canDim && !capability.temperatureRange && !capability.effects.length" class="scale-note">这盏灯未上报可用的调光能力,目前仅提供开关。</p>
</template>
<style scoped>
.standard-light-select { width: 100%; min-height: 40px; padding: 8px 12px; border: 1px solid #d9e6de; border-radius: 10px; background: #fff; color: #174d40; font: inherit; font-size: 13px; }
.standard-light-select:disabled { opacity: .5; }
</style>