111
This commit is contained in:
@@ -175,6 +175,23 @@ export type HomeAssistantDeviceSwitchOutput = {
|
|||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HomeAssistantLightTransition = {
|
||||||
|
success: boolean
|
||||||
|
running: boolean
|
||||||
|
mode: 'none' | 'sleep' | 'wake'
|
||||||
|
brightnessPercent: number
|
||||||
|
targetPercent: number
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HomeAssistantSpeakerDemo = {
|
||||||
|
audioUrl: string
|
||||||
|
speakerOnline: boolean
|
||||||
|
directPlaybackSupported: boolean
|
||||||
|
commandAccepted: boolean
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
export type HomeAssistantStatus = {
|
export type HomeAssistantStatus = {
|
||||||
connected: boolean
|
connected: boolean
|
||||||
message: string
|
message: string
|
||||||
@@ -236,6 +253,18 @@ export const healthApi = {
|
|||||||
http.post<never, HealthAssessmentOutput>('/api/health/assessment', payload),
|
http.post<never, HealthAssessmentOutput>('/api/health/assessment', payload),
|
||||||
homeAssistantStatus: () =>
|
homeAssistantStatus: () =>
|
||||||
http.get<never, HomeAssistantStatus>('/api/health/home-assistant/status'),
|
http.get<never, HomeAssistantStatus>('/api/health/home-assistant/status'),
|
||||||
|
lightTransitionStatus: () =>
|
||||||
|
http.get<never, HomeAssistantLightTransition>('/api/health/home-assistant/lights/transition'),
|
||||||
|
startSleepLight: () =>
|
||||||
|
http.post<never, HomeAssistantLightTransition>('/api/health/home-assistant/lights/sleep'),
|
||||||
|
startWakeLight: () =>
|
||||||
|
http.post<never, HomeAssistantLightTransition>('/api/health/home-assistant/lights/wake'),
|
||||||
|
stopLightTransition: () =>
|
||||||
|
http.post<never, HomeAssistantLightTransition>('/api/health/home-assistant/lights/stop'),
|
||||||
|
speakerDemoStatus: () =>
|
||||||
|
http.get<never, HomeAssistantSpeakerDemo>('/api/health/home-assistant/speaker/demo'),
|
||||||
|
playSpeakerDemo: () =>
|
||||||
|
http.post<never, HomeAssistantSpeakerDemo>('/api/health/home-assistant/speaker/play-demo'),
|
||||||
executeSleepScene: () =>
|
executeSleepScene: () =>
|
||||||
http.post<never, HomeAssistantSceneResult>('/api/health/home-assistant/scenes/sleep'),
|
http.post<never, HomeAssistantSceneResult>('/api/health/home-assistant/scenes/sleep'),
|
||||||
executeAllOffScene: () =>
|
executeAllOffScene: () =>
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
<div class="startup-param-badge">
|
<div class="startup-param-badge">
|
||||||
<span class="lbl">助眠灯光</span>
|
<span class="lbl">助眠灯光</span>
|
||||||
<strong>{{ dashboard?.sceneConfig?.lightBrightnessPercent || 20 }}% · {{ dashboard?.sceneConfig?.lightColorTemperatureKelvin || 2700 }}K</strong>
|
<strong>50%渐暗至关闭 · {{ dashboard?.sceneConfig?.lightColorTemperatureKelvin || 2700 }}K</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="startup-param-badge">
|
<div class="startup-param-badge">
|
||||||
<span class="lbl">助眠音箱</span>
|
<span class="lbl">助眠音箱</span>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
Button as VanButton,
|
Button as VanButton,
|
||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
healthApi,
|
healthApi,
|
||||||
type HomeAssistantDiscoveredDevice,
|
type HomeAssistantDiscoveredDevice,
|
||||||
|
type HomeAssistantLightTransition,
|
||||||
|
type HomeAssistantSpeakerDemo,
|
||||||
type HomeAssistantStatus
|
type HomeAssistantStatus
|
||||||
} from '../services/health'
|
} from '../services/health'
|
||||||
|
|
||||||
@@ -22,6 +24,12 @@ const settingParam = ref<string | null>(null)
|
|||||||
const switchingDeviceId = ref<string | null>(null)
|
const switchingDeviceId = ref<string | null>(null)
|
||||||
const lastUpdatedAt = ref('')
|
const lastUpdatedAt = ref('')
|
||||||
const actionFeedback = ref<{ type: 'success' | 'fail'; message: string }>()
|
const actionFeedback = ref<{ type: 'success' | 'fail'; message: string }>()
|
||||||
|
const lightTransition = ref<HomeAssistantLightTransition>()
|
||||||
|
const lightActionBusy = ref(false)
|
||||||
|
const speakerDemo = ref<HomeAssistantSpeakerDemo>()
|
||||||
|
const speakerDemoBusy = ref(false)
|
||||||
|
let lightProgressTimer: ReturnType<typeof setInterval> | undefined
|
||||||
|
let lightPolling = false
|
||||||
|
|
||||||
const parameterLabels: Record<string, string> = {
|
const parameterLabels: Record<string, string> = {
|
||||||
humidity: '目标湿度',
|
humidity: '目标湿度',
|
||||||
@@ -32,6 +40,13 @@ const parameterLabels: Record<string, string> = {
|
|||||||
color_temp: '灯光色温',
|
color_temp: '灯光色温',
|
||||||
volume: '音箱音量',
|
volume: '音箱音量',
|
||||||
speaker_sleep_mode: '音箱睡眠模式',
|
speaker_sleep_mode: '音箱睡眠模式',
|
||||||
|
fragrance_duration: '出香时长',
|
||||||
|
fragrance_interval: '出香间隔',
|
||||||
|
fragrance_light: '香薰氛围灯',
|
||||||
|
fragrance_quick_release: '快速出香',
|
||||||
|
fragrance_auto_release: '人来增香',
|
||||||
|
fragrance_auto_light: '感应夜灯',
|
||||||
|
fragrance_light_auto_off: '氛围灯自动关闭',
|
||||||
mattress_left_heating: '床垫左侧加热',
|
mattress_left_heating: '床垫左侧加热',
|
||||||
mattress_right_heating: '床垫右侧加热',
|
mattress_right_heating: '床垫右侧加热',
|
||||||
mattress_left_mode: '床垫左侧承托模式',
|
mattress_left_mode: '床垫左侧承托模式',
|
||||||
@@ -43,10 +58,12 @@ function setFeedback(type: 'success' | 'fail', message: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const operationBusy = computed(() =>
|
const operationBusy = computed(() =>
|
||||||
loading.value || Boolean(sceneRunning.value) || Boolean(settingParam.value) || Boolean(switchingDeviceId.value)
|
loading.value || lightActionBusy.value || speakerDemoBusy.value || Boolean(sceneRunning.value) || Boolean(settingParam.value) || Boolean(switchingDeviceId.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentOperationText = computed(() => {
|
const currentOperationText = computed(() => {
|
||||||
|
if (speakerDemoBusy.value) return '正在尝试播放示例音频…'
|
||||||
|
if (lightActionBusy.value) return '正在切换灯光渐变模式…'
|
||||||
if (sceneRunning.value === 'sleep') return '正在启动全屋舒睡模式…'
|
if (sceneRunning.value === 'sleep') return '正在启动全屋舒睡模式…'
|
||||||
if (sceneRunning.value === 'all-off') return '正在关闭场景设备…'
|
if (sceneRunning.value === 'all-off') return '正在关闭场景设备…'
|
||||||
if (switchingDeviceId.value) return '正在同步设备开关状态…'
|
if (switchingDeviceId.value) return '正在同步设备开关状态…'
|
||||||
@@ -68,13 +85,21 @@ const localParams = reactive({
|
|||||||
dehumidifierUv: false,
|
dehumidifierUv: false,
|
||||||
|
|
||||||
// 氛围灯
|
// 氛围灯
|
||||||
lightBrightness: 20,
|
lightBrightness: 50,
|
||||||
lightColorTemp: 2700,
|
lightColorTemp: 2700,
|
||||||
|
|
||||||
// 音箱
|
// 音箱
|
||||||
speakerVolume: 15,
|
speakerVolume: 15,
|
||||||
speakerSleepMode: true,
|
speakerSleepMode: true,
|
||||||
|
|
||||||
|
// 香薰机
|
||||||
|
fragranceDuration: 2,
|
||||||
|
fragranceInterval: 20,
|
||||||
|
fragranceLight: false,
|
||||||
|
fragranceAutoRelease: false,
|
||||||
|
fragranceAutoLight: false,
|
||||||
|
fragranceLightAutoOff: false,
|
||||||
|
|
||||||
// 床垫
|
// 床垫
|
||||||
mattressLeftHeating: 1,
|
mattressLeftHeating: 1,
|
||||||
mattressRightHeating: 1,
|
mattressRightHeating: 1,
|
||||||
@@ -116,10 +141,26 @@ async function loadStatus(notify = false) {
|
|||||||
if (volume !== undefined) localParams.speakerVolume = clamp(Math.round(volume * 100), 0, 100)
|
if (volume !== undefined) localParams.speakerVolume = clamp(Math.round(volume * 100), 0, 100)
|
||||||
localParams.speakerSleepMode = controls.speaker_sleep_mode?.state === 'on'
|
localParams.speakerSleepMode = controls.speaker_sleep_mode?.state === 'on'
|
||||||
|
|
||||||
|
localParams.fragranceDuration = clamp(numberValue(controls.fragrance_duration?.state) ?? 2, 2, 6)
|
||||||
|
localParams.fragranceInterval = clamp(numberValue(controls.fragrance_interval?.state) ?? 20, 10, 20)
|
||||||
|
localParams.fragranceLight = controls.fragrance_light?.state === 'on'
|
||||||
|
localParams.fragranceAutoRelease = controls.fragrance_auto_release?.state === 'on'
|
||||||
|
localParams.fragranceAutoLight = controls.fragrance_auto_light?.state === 'on'
|
||||||
|
localParams.fragranceLightAutoOff = controls.fragrance_light_auto_off?.state === 'on'
|
||||||
|
|
||||||
localParams.mattressLeftHeating = clamp(numberValue(controls.mattress_left_heating?.state) ?? 1, 0, 4)
|
localParams.mattressLeftHeating = clamp(numberValue(controls.mattress_left_heating?.state) ?? 1, 0, 4)
|
||||||
localParams.mattressRightHeating = clamp(numberValue(controls.mattress_right_heating?.state) ?? 1, 0, 4)
|
localParams.mattressRightHeating = clamp(numberValue(controls.mattress_right_heating?.state) ?? 1, 0, 4)
|
||||||
if (controls.mattress_left_mode?.state) localParams.mattressLeftMode = controls.mattress_left_mode.state
|
if (controls.mattress_left_mode?.state) localParams.mattressLeftMode = controls.mattress_left_mode.state
|
||||||
if (controls.mattress_right_mode?.state) localParams.mattressRightMode = controls.mattress_right_mode.state
|
if (controls.mattress_right_mode?.state) localParams.mattressRightMode = controls.mattress_right_mode.state
|
||||||
|
await syncLightTransitionStatus()
|
||||||
|
try {
|
||||||
|
speakerDemo.value = await healthApi.speakerDemoStatus()
|
||||||
|
} catch (err) {
|
||||||
|
speakerDemo.value = {
|
||||||
|
audioUrl: '', speakerOnline: false, directPlaybackSupported: false, commandAccepted: false,
|
||||||
|
message: err instanceof Error ? err.message : '音箱能力检测失败'
|
||||||
|
}
|
||||||
|
}
|
||||||
lastUpdatedAt.value = new Intl.DateTimeFormat('zh-CN', {
|
lastUpdatedAt.value = new Intl.DateTimeFormat('zh-CN', {
|
||||||
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
|
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
|
||||||
}).format(new Date())
|
}).format(new Date())
|
||||||
@@ -136,6 +177,85 @@ async function loadStatus(notify = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function syncLightTransitionStatus() {
|
||||||
|
const status = await healthApi.lightTransitionStatus()
|
||||||
|
lightTransition.value = status
|
||||||
|
if (status.running) {
|
||||||
|
localParams.lightBrightness = Math.max(1, status.brightnessPercent)
|
||||||
|
if (!lightProgressTimer) lightProgressTimer = setInterval(pollLightTransition, 5000)
|
||||||
|
} else if (lightProgressTimer) {
|
||||||
|
clearInterval(lightProgressTimer)
|
||||||
|
lightProgressTimer = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollLightTransition() {
|
||||||
|
if (lightPolling) return
|
||||||
|
lightPolling = true
|
||||||
|
try {
|
||||||
|
const wasRunning = lightTransition.value?.running
|
||||||
|
await syncLightTransitionStatus()
|
||||||
|
if (wasRunning && !lightTransition.value?.running) {
|
||||||
|
await loadStatus()
|
||||||
|
if (lightTransition.value?.message) {
|
||||||
|
setFeedback(lightTransition.value.success ? 'success' : 'fail', lightTransition.value.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setFeedback('fail', err instanceof Error ? err.message : '灯光状态刷新失败')
|
||||||
|
} finally {
|
||||||
|
lightPolling = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function controlLightTransition(action: 'sleep' | 'wake' | 'stop') {
|
||||||
|
lightActionBusy.value = true
|
||||||
|
const loadingToast = showLoadingToast({
|
||||||
|
message: action === 'sleep' ? '正在启动助眠渐暗…' : action === 'wake' ? '正在启动晨起唤醒…' : '正在停止灯光渐变…',
|
||||||
|
forbidClick: true,
|
||||||
|
duration: 0
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const result = action === 'sleep'
|
||||||
|
? await healthApi.startSleepLight()
|
||||||
|
: action === 'wake'
|
||||||
|
? await healthApi.startWakeLight()
|
||||||
|
: await healthApi.stopLightTransition()
|
||||||
|
if (!result.success) throw new Error(result.message)
|
||||||
|
lightTransition.value = result
|
||||||
|
await loadStatus()
|
||||||
|
setFeedback('success', result.message)
|
||||||
|
showSuccessToast(result.message)
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : '灯光操作失败'
|
||||||
|
setFeedback('fail', message)
|
||||||
|
showFailToast(message)
|
||||||
|
} finally {
|
||||||
|
loadingToast.close()
|
||||||
|
lightActionBusy.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryPlaySpeakerDemo() {
|
||||||
|
if (!speakerDemo.value?.directPlaybackSupported || operationBusy.value) return
|
||||||
|
speakerDemoBusy.value = true
|
||||||
|
const loadingToast = showLoadingToast({ message: '正在尝试投放到音箱…', forbidClick: true, duration: 0 })
|
||||||
|
try {
|
||||||
|
const result = await healthApi.playSpeakerDemo()
|
||||||
|
speakerDemo.value = result
|
||||||
|
if (!result.commandAccepted) throw new Error(result.message)
|
||||||
|
setFeedback('success', result.message)
|
||||||
|
showSuccessToast('指令已发送,请确认音箱实际出声')
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : '投放失败'
|
||||||
|
setFeedback('fail', message)
|
||||||
|
showFailToast(message)
|
||||||
|
} finally {
|
||||||
|
loadingToast.close()
|
||||||
|
speakerDemoBusy.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 查找具体设备
|
// 查找具体设备
|
||||||
const dehumidifierDevice = computed(() =>
|
const dehumidifierDevice = computed(() =>
|
||||||
haStatus.value?.devices.find(d => d.category === 'EnvironmentControl')
|
haStatus.value?.devices.find(d => d.category === 'EnvironmentControl')
|
||||||
@@ -313,6 +433,10 @@ function handleBack() {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadStatus()
|
loadStatus()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (lightProgressTimer) clearInterval(lightProgressTimer)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -509,10 +633,24 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="params-body">
|
<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 || !lightDevice.online" @click="controlLightTransition('sleep')">🌙 开始助眠渐暗</button>
|
||||||
|
<button type="button" :disabled="operationBusy || !lightDevice.online" @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-row">
|
||||||
<div class="param-header">
|
<div class="param-header">
|
||||||
<span class="p-title">助眠灯光亮度</span>
|
<span class="p-title">灯光亮度</span>
|
||||||
<strong class="p-val">{{ localParams.lightBrightness }}%</strong>
|
<strong class="p-val">{{ localParams.lightBrightness }}%</strong>
|
||||||
</div>
|
</div>
|
||||||
<VanSlider
|
<VanSlider
|
||||||
@@ -577,6 +715,17 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="params-body">
|
<div class="params-body">
|
||||||
|
<div class="speaker-demo-panel">
|
||||||
|
<strong>OSS 音频播放 Demo</strong>
|
||||||
|
<small>先在手机试听,再检测能否投放到 Sound Pro。</small>
|
||||||
|
<audio v-if="speakerDemo?.audioUrl" controls preload="none" :src="speakerDemo.audioUrl">
|
||||||
|
当前浏览器不支持播放音频
|
||||||
|
</audio>
|
||||||
|
<p>{{ speakerDemo?.message || '正在检测音箱播放能力…' }}</p>
|
||||||
|
<button type="button" :disabled="operationBusy || !speakerDemo?.directPlaybackSupported" @click="tryPlaySpeakerDemo">
|
||||||
|
{{ speakerDemo?.directPlaybackSupported ? '尝试音箱播放' : '当前音箱集成暂不支持投放' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<!-- 音量滑块 -->
|
<!-- 音量滑块 -->
|
||||||
<div class="param-row">
|
<div class="param-row">
|
||||||
<div class="param-header">
|
<div class="param-header">
|
||||||
@@ -640,8 +789,103 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div class="params-body">
|
<div class="params-body">
|
||||||
<div class="param-row">
|
<div class="param-row">
|
||||||
<span class="p-title">当前可控能力</span>
|
<div class="param-header">
|
||||||
<p class="p-tip">当前 Home Assistant 集成仅提供香氛机开关与氛围灯控制;浓度请在米家设备端预设。</p>
|
<span class="p-title">单次出香时长</span>
|
||||||
|
<strong class="p-val">{{ localParams.fragranceDuration }} 秒</strong>
|
||||||
|
</div>
|
||||||
|
<VanSlider
|
||||||
|
v-model="localParams.fragranceDuration"
|
||||||
|
:min="2"
|
||||||
|
:max="6"
|
||||||
|
:step="1"
|
||||||
|
:disabled="operationBusy"
|
||||||
|
@change="applyParam(fragranceDevice.deviceId, 'fragrance_duration', localParams.fragranceDuration)"
|
||||||
|
/>
|
||||||
|
<div class="range-labels"><span>2 秒</span><span>4 秒</span><span>6 秒</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-row">
|
||||||
|
<div class="param-header">
|
||||||
|
<span class="p-title">自动出香间隔</span>
|
||||||
|
<strong class="p-val">{{ localParams.fragranceInterval }} 分钟</strong>
|
||||||
|
</div>
|
||||||
|
<VanSlider
|
||||||
|
v-model="localParams.fragranceInterval"
|
||||||
|
:min="10"
|
||||||
|
:max="20"
|
||||||
|
:step="5"
|
||||||
|
:disabled="operationBusy"
|
||||||
|
@change="applyParam(fragranceDevice.deviceId, 'fragrance_interval', localParams.fragranceInterval)"
|
||||||
|
/>
|
||||||
|
<div class="range-labels"><span>10 分钟</span><span>15 分钟</span><span>20 分钟</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-row inline-row">
|
||||||
|
<div>
|
||||||
|
<div class="p-title">香薰氛围灯</div>
|
||||||
|
<small class="p-tip">与香薰释放独立控制</small>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="mini-toggle"
|
||||||
|
:class="{ active: localParams.fragranceLight }"
|
||||||
|
:disabled="operationBusy"
|
||||||
|
@click="localParams.fragranceLight = !localParams.fragranceLight; applyParam(fragranceDevice.deviceId, 'fragrance_light', localParams.fragranceLight)"
|
||||||
|
>{{ localParams.fragranceLight ? '已开启' : '已关闭' }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-row inline-row">
|
||||||
|
<div>
|
||||||
|
<div class="p-title">人来增香</div>
|
||||||
|
<small class="p-tip">检测到有人活动时自动增香</small>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="mini-toggle"
|
||||||
|
:class="{ active: localParams.fragranceAutoRelease }"
|
||||||
|
:disabled="operationBusy"
|
||||||
|
@click="localParams.fragranceAutoRelease = !localParams.fragranceAutoRelease; applyParam(fragranceDevice.deviceId, 'fragrance_auto_release', localParams.fragranceAutoRelease)"
|
||||||
|
>{{ localParams.fragranceAutoRelease ? '已开启' : '已关闭' }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-row inline-row">
|
||||||
|
<div>
|
||||||
|
<div class="p-title">感应夜灯</div>
|
||||||
|
<small class="p-tip">有人移动时自动开启柔光</small>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="mini-toggle"
|
||||||
|
:class="{ active: localParams.fragranceAutoLight }"
|
||||||
|
:disabled="operationBusy"
|
||||||
|
@click="localParams.fragranceAutoLight = !localParams.fragranceAutoLight; applyParam(fragranceDevice.deviceId, 'fragrance_auto_light', localParams.fragranceAutoLight)"
|
||||||
|
>{{ localParams.fragranceAutoLight ? '已开启' : '已关闭' }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-row inline-row">
|
||||||
|
<div>
|
||||||
|
<div class="p-title">无人时自动关灯</div>
|
||||||
|
<small class="p-tip">关闭时氛围灯保持常亮</small>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="mini-toggle"
|
||||||
|
:class="{ active: localParams.fragranceLightAutoOff }"
|
||||||
|
:disabled="operationBusy"
|
||||||
|
@click="localParams.fragranceLightAutoOff = !localParams.fragranceLightAutoOff; applyParam(fragranceDevice.deviceId, 'fragrance_light_auto_off', localParams.fragranceLightAutoOff)"
|
||||||
|
>{{ localParams.fragranceLightAutoOff ? '已开启' : '已关闭' }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-row inline-row">
|
||||||
|
<div>
|
||||||
|
<div class="p-title">立即出香</div>
|
||||||
|
<small class="p-tip">手动触发一次当前时长的香氛释放</small>
|
||||||
|
</div>
|
||||||
|
<VanButton
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
plain
|
||||||
|
color="#0c5045"
|
||||||
|
:loading="settingParam === `${fragranceDevice.deviceId}_fragrance_quick_release`"
|
||||||
|
:disabled="operationBusy"
|
||||||
|
@click="applyParam(fragranceDevice.deviceId, 'fragrance_quick_release', true)"
|
||||||
|
>快速出香</VanButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1123,6 +1367,76 @@ onMounted(() => {
|
|||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.light-mode-panel {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff8ec;
|
||||||
|
border: 1px solid #f2e3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-mode-panel strong,
|
||||||
|
.light-mode-panel small {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-mode-panel strong {
|
||||||
|
color: #684b24;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-mode-panel small {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #8b7759;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-mode-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-mode-actions button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #dab989;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #fff;
|
||||||
|
color: #795320;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-mode-actions button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speaker-demo-panel {
|
||||||
|
padding: 13px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f3f8f7;
|
||||||
|
border: 1px solid #d9e9e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speaker-demo-panel strong,
|
||||||
|
.speaker-demo-panel small,
|
||||||
|
.speaker-demo-panel audio {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speaker-demo-panel strong { font-size: 13px; color: #174d44; }
|
||||||
|
.speaker-demo-panel small { margin-top: 4px; color: #647b75; font-size: 11px; }
|
||||||
|
.speaker-demo-panel audio { width: 100%; margin-top: 10px; height: 38px; }
|
||||||
|
.speaker-demo-panel p { margin: 10px 0; color: #526a64; font-size: 11px; line-height: 1.5; }
|
||||||
|
.speaker-demo-panel button {
|
||||||
|
width: 100%; padding: 9px 10px; border: 0; border-radius: 9px;
|
||||||
|
background: #0c5045; color: #fff; font-size: 12px; font-weight: 600;
|
||||||
|
}
|
||||||
|
.speaker-demo-panel button:disabled { background: #b8c5c1; }
|
||||||
|
|
||||||
/* 滑块样式:左右各预留 10px 间距,使滑块在 0% 和 100% 端点时,外凸的圆形按钮完整居中且不被裁剪 */
|
/* 滑块样式:左右各预留 10px 间距,使滑块在 0% 和 100% 端点时,外凸的圆形按钮完整居中且不被裁剪 */
|
||||||
.param-row :deep(.van-slider) {
|
.param-row :deep(.van-slider) {
|
||||||
margin: 10px 10px 14px 10px;
|
margin: 10px 10px 14px 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user