@@ -15,6 +15,7 @@ import {
type HomeAssistantLightTransition ,
type HomeAssistantStatus ,
type MattressComfort ,
type TcmMusicTransition ,
type TcmAssessmentResult
} from '../services/health'
@@ -29,9 +30,15 @@ const switchingDeviceId = ref<string | null>(null)
const lastUpdatedAt = ref ( '' )
const actionFeedback = ref < { type : 'success' | 'fail' ; message : string } > ( )
const lightTransition = ref < HomeAssistantLightTransition > ( )
const musicTransition = ref < TcmMusicTransition > ( )
const lightActionBusy = ref ( false )
let lightProgressTimer : ReturnType < typeof setInterval > | undefined
let lightPolling = false
let musicProgressTimer : ReturnType < typeof setInterval > | undefined
let musicPolling = false
const sceneRefreshTimers : ReturnType < typeof setTimeout > [ ] = [ ]
const currentUserId = ref < number > ( )
const mattressActiveSide = ref < 'left' | 'right' | null > ( null )
const parameterLabels : Record < string , string > = {
humidity : '目标湿度' ,
@@ -55,6 +62,7 @@ const parameterLabels: Record<string, string> = {
mattress _right _mode : '床垫右侧承托模式'
}
const mattressFirmnessOptions = [ '最软' , '很软' , '稍软' , '稍硬' , '很硬' , '最硬' ]
const mattressModeOptions = [ '无' , '瑜伽助眠' , '智能适配' , '智能腰托' ]
const mattressFirmnessZones = [
{ key : 'left_shoulder' , label : '左肩' } , { key : 'right_shoulder' , label : '右肩' } ,
{ key : 'left_back' , label : '左背' } , { key : 'right_back' , label : '右背' } ,
@@ -100,7 +108,7 @@ const localParams = reactive({
lightColorTemp : 2700 ,
// 音箱
speakerVolume : 15 ,
speakerVolume : 50 ,
speakerSleepMode : true ,
// 香薰机
@@ -135,8 +143,37 @@ function numberValue(value: unknown): number | undefined {
return undefined
}
async function loadStatus ( notify = false ) {
loading . value = true
function mattressModeStorageKey ( ) : string | undefined {
const deviceId = mattressDevice . value ? . deviceId
return currentUserId . value && deviceId
? ` evergreen-mattress-modes- ${ currentUserId . value } - ${ deviceId } `
: undefined
}
function readMattressModePreference ( ) : { left : string ; right : string } | undefined {
const key = mattressModeStorageKey ( )
if ( ! key ) return undefined
try {
const saved = JSON . parse ( localStorage . getItem ( key ) || 'null' )
return mattressModeOptions . includes ( saved ? . left ) && mattressModeOptions . includes ( saved ? . right ) ? saved : undefined
} catch {
return undefined
}
}
function saveMattressModePreference ( ) {
const key = mattressModeStorageKey ( )
if ( ! key ) return
try {
localStorage . setItem ( key , JSON . stringify ( {
left : localParams . mattressLeftMode ,
right : localParams . mattressRightMode
} ) )
} catch { /* 浏览器禁止本地存储时,本次页面内的预选仍可使用。 */ }
}
async function loadStatus ( notify = false , background = false ) {
if ( ! background ) loading . value = true
try {
const [ status , dashboard ] = await Promise . all ( [
healthApi . homeAssistantStatus ( ) ,
@@ -144,6 +181,7 @@ async function loadStatus(notify = false) {
] )
haStatus . value = status
if ( dashboard ) {
currentUserId . value = dashboard . user . id
tcmResult . value = dashboard . tcmResult
mattressComfort . value = dashboard . mattressComfort
}
@@ -173,8 +211,16 @@ async function loadStatus(notify = false) {
localParams . mattressLeftHeating = clamp ( numberValue ( controls . mattress _left _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 _right _mode ? . state ) localParams . mattressRightMode = controls . mattress _right _mode . state
const actualLeftMode = controls . mattress _left _mode ? . state
const actualRightMode = controls . mattress _right _mode ? . state
const preferredModes = readMattressModePreference ( )
mattressActiveSide . value = actualRightMode && mattressModeOptions . includes ( actualRightMode ) && actualRightMode !== '无'
? 'right'
: actualLeftMode && mattressModeOptions . includes ( actualLeftMode ) && actualLeftMode !== '无' ? 'left' : null
localParams . mattressLeftMode = mattressActiveSide . value === 'left'
? actualLeftMode ! : preferredModes ? . left ? ? '智能适配'
localParams . mattressRightMode = mattressActiveSide . value === 'right'
? actualRightMode ! : preferredModes ? . right ? ? '智能适配'
for ( const zone of mattressFirmnessZones ) {
const state = controls [ ` mattress_firmness_ ${ zone . key } ` ] ? . state
if ( state && state !== 'unavailable' && state !== 'unknown' )
@@ -184,6 +230,7 @@ async function loadStatus(notify = false) {
if ( controls . mattress _massage _right _part ? . state ) localParams . mattressMassageRightPart = controls . mattress _massage _right _part . state
if ( controls . mattress _massage _level ? . state ) localParams . mattressMassageLevel = controls . mattress _massage _level . state
await syncLightTransitionStatus ( )
await syncMusicTransitionStatus ( ) . catch ( ( ) => undefined )
lastUpdatedAt . value = new Intl . DateTimeFormat ( 'zh-CN' , {
hour : '2-digit' , minute : '2-digit' , second : '2-digit' , hour12 : false
} ) . format ( new Date ( ) )
@@ -193,10 +240,42 @@ async function loadStatus(notify = false) {
}
} catch ( err ) {
const message = err instanceof Error ? err . message : '获取设备状态失败'
setFeedback ( 'fail' , message )
showFailToast ( message )
if ( ! background ) {
setFeedback ( 'fail' , message )
showFailToast ( message )
}
} finally {
loading . value = false
if ( ! background ) loading . value = false
}
}
async function syncMusicTransitionStatus ( ) {
const status = await healthApi . musicTransitionStatus ( )
musicTransition . value = status
if ( status . hasVolume ) localParams . speakerVolume = clamp ( status . volumePercent , 0 , 100 )
if ( status . running && ! musicProgressTimer )
musicProgressTimer = setInterval ( pollMusicTransition , 5000 )
else if ( ! status . running && musicProgressTimer ) {
clearInterval ( musicProgressTimer )
musicProgressTimer = undefined
}
}
async function pollMusicTransition ( ) {
if ( musicPolling ) return
musicPolling = true
try { await syncMusicTransitionStatus ( ) }
catch { /* 网络短暂波动时保留最后一次已下发的音量,下个周期继续读取。 */ }
finally { musicPolling = false }
}
function scheduleSceneStatusRefresh ( ) {
for ( const timer of sceneRefreshTimers ) clearTimeout ( timer )
sceneRefreshTimers . length = 0
for ( const delay of [ 2000 , 5000 , 10000 ] ) {
sceneRefreshTimers . push ( setTimeout ( ( ) => {
if ( ! operationBusy . value ) void loadStatus ( false , true )
} , delay ) )
}
}
@@ -372,7 +451,7 @@ async function controlSpeaker(device: HomeAssistantDiscoveredDevice, play: boole
}
// 调节参数 API
async function applyParam ( deviceId : string , paramType : string , val : unknown , entityId ? : string ) {
async function applyParam ( deviceId : string , paramType : string , val : unknown , entityId ? : string ) : Promise < boolean > {
const key = ` ${ deviceId } _ ${ paramType } `
settingParam . value = key
const label = parameterLabels [ paramType ]
@@ -391,14 +470,17 @@ async function applyParam(deviceId: string, paramType: string, val: unknown, ent
} )
const message = res . message || ` ${ label } 设置成功 `
if ( ! res . success ) throw new Error ( message )
if ( paramType ! == 'volume' ) await loadStatus ( )
if ( paramType = == 'volume' ) await syncMusicTransitionStatus ( ) . catch ( ( ) => undefined )
else await loadStatus ( )
setFeedback ( 'success' , message )
showSuccessToast ( message )
return true
} catch ( err ) {
const message = err instanceof Error ? err . message : ` ${ label } 设置失败 `
await loadStatus ( )
setFeedback ( 'fail' , message )
showFailToast ( message )
return false
} finally {
loadingToast . close ( )
settingParam . value = null
@@ -408,17 +490,13 @@ async function applyParam(deviceId: string, paramType: string, val: unknown, ent
async function setMattressMode ( side : 'left' | 'right' , mode : string ) {
const device = mattressDevice . value
if ( ! device ) return
if ( side === 'left' ) {
localParams . mattressLeftMode = mode
if ( mode !== '无' ) localParams . mattressRightMode = '无'
await applyParam ( device . deviceId , 'mattress_left_mode' , mode )
return
}
localParams . mattressRightMode = mode
if ( mode !== '无' ) localParams . mattressLeftMode = '无'
await applyParam ( device . deviceId , 'mattress_right_mode' , mode )
const succeeded = await applyParam ( device . deviceId , ` mattress_ ${ side } _mode ` , mode )
if ( ! succeeded ) return
if ( side === 'left' ) localParams . mattressLeftMode = mode
else localParams . mattressRightMode = mode
if ( mode !== '无' ) mattressActiveSide . value = side
else if ( mattressActiveSide . value === side ) mattressActiveSide . value = null
saveMattressModePreference ( )
}
async function setMattressSelect ( parameterType : string , event : Event ) {
@@ -458,8 +536,9 @@ async function executeScene(scene: 'sleep' | 'all-off') {
: await healthApi . executeAllOffScene ( )
if ( ! res . success ) throw new Error ( res . message || '场景执行失败' )
await loadStatus ( )
setFeedback ( 'success' , res . message )
showSuccessToast ( res . message )
setFeedback ( res . status === 'Partial' ? 'fail' : 'success' , res . message )
if ( res . status === 'Partial' ) showFailToast ( res . message )
else showSuccessToast ( res . message )
} catch ( err ) {
const message = err instanceof Error ? err . message : '场景执行失败'
await loadStatus ( )
@@ -468,6 +547,7 @@ async function executeScene(scene: 'sleep' | 'all-off') {
} finally {
loadingToast . close ( )
sceneRunning . value = undefined
scheduleSceneStatusRefresh ( )
}
}
@@ -485,6 +565,8 @@ onMounted(() => {
onUnmounted ( ( ) => {
if ( lightProgressTimer ) clearInterval ( lightProgressTimer )
if ( musicProgressTimer ) clearInterval ( musicProgressTimer )
for ( const timer of sceneRefreshTimers ) clearTimeout ( timer )
} )
< / script >
@@ -778,7 +860,7 @@ onUnmounted(() => {
<!-- 音量滑块 -- >
< div class = "param-row" >
< div class = "param-header" >
< span class = "p-title" > 手动播放音量 < / span >
< span class = "p-title" > 音箱音量 ( MiAirX 已下发 ) < / span >
< strong class = "p-val" > { { localParams . speakerVolume } } % < / strong >
< / div >
< VanSlider
@@ -794,7 +876,7 @@ onUnmounted(() => {
< span > 50 % 适中聆听 < / span >
< span > 100 % 满格音量 < / span >
< / div >
< small class = "p-tip" > 手动调整音量会停止本次自动渐弱 < / small >
< small class = "p-tip" > { { musicTransition ? . running ? '自动渐弱中,每 5 秒更新一次显示' : ' 手动调整音量会停止本次自动渐弱' } } < / small >
< / div >
< div class = "param-row inline-row" >
@@ -837,6 +919,8 @@ onUnmounted(() => {
< / VanButton >
< / div >
< small class = "p-tip" > 上方 “ 释放中 / 已关闭 ” 只表示香薰主开关 ; 感应夜灯在下方单独显示和控制 。 < / small >
< div class = "params-body" >
< div class = "param-row" >
< div class = "param-header" >
@@ -1008,17 +1092,17 @@ onUnmounted(() => {
< div class = "param-row" >
< p class = "mattress-mode-note" >
左右承托 共用气泵 , 同一时间仅能运行一侧 ; 启动一侧后 , 另一侧会自动停止 。
左右可分别预选模式 , 但 共用气泵, 同一时间仅最近启动的一侧实际运行 ; 另一侧保留预选 , 处于待机状态 。
< / p >
< / div >
< div class = "param-row" >
< span class = "p-title" > 左侧承托模式 < / span >
< div class = "btn-group" >
< span class = "p-title" > 左侧承托模式 · { { mattressActiveSide === 'left' ? '运行中' : '预选待机' } } < / span >
< div class = "btn-group mattress-mode-buttons " >
< button
v-for = "mode in ['无', '瑜伽助眠', '智能适配', '智能腰托'] "
v-for = "mode in mattressModeOptions "
:key = "`left-${mode}`"
: class = "{ active: localParams.mattressLeftMode === mode }"
: class = "{ active: localParams.mattressLeftMode === mode, running: mattressActiveSide === 'left' && localParams.mattressLeftMode === mode }"
:disabled = "operationBusy"
@click ="setMattressMode('left', mode)"
> { { mode } } < / button >
@@ -1026,12 +1110,12 @@ onUnmounted(() => {
< / div >
< div class = "param-row" >
< span class = "p-title" > 右侧承托模式 < / span >
< div class = "btn-group" >
< span class = "p-title" > 右侧承托模式 · { { mattressActiveSide === 'right' ? '运行中' : '预选待机' } } < / span >
< div class = "btn-group mattress-mode-buttons " >
< button
v-for = "mode in ['无', '瑜伽助眠', '智能适配', '智能腰托'] "
v-for = "mode in mattressModeOptions "
:key = "`right-${mode}`"
: class = "{ active: localParams.mattressRightMode === mode }"
: class = "{ active: localParams.mattressRightMode === mode, running: mattressActiveSide === 'right' && localParams.mattressRightMode === mode }"
:disabled = "operationBusy"
@click ="setMattressMode('right', mode)"
> { { mode } } < / button >
@@ -1598,6 +1682,12 @@ onUnmounted(() => {
font - weight : 700 ;
}
. mattress - mode - buttons button . active : not ( . running ) {
background : # e4f1ec ;
color : # 0 c5045 ;
border - color : # 87 b9a7 ;
}
. btn - group button : disabled ,
. mini - toggle : disabled {
opacity : 0.55 ;