@@ -1,7 +1,13 @@
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref } from 'vue'
import { useRouter } from 'vue-router'
import { Button as VanButton , showToast , Slider as VanSlider } from 'vant'
import {
Button as VanButton ,
showFailToast ,
showLoadingToast ,
showSuccessToast ,
Slider as VanSlider
} from 'vant'
import {
healthApi ,
type HomeAssistantDiscoveredDevice ,
@@ -14,6 +20,44 @@ const haStatus = ref<HomeAssistantStatus>()
const sceneRunning = ref < 'sleep' | 'all-off' > ( )
const settingParam = ref < string | null > ( null )
const switchingDeviceId = ref < string | null > ( null )
const lastUpdatedAt = ref ( '' )
const actionFeedback = ref < { type : 'success' | 'fail' ; message : string } > ( )
const parameterLabels : Record < string , string > = {
humidity : '目标湿度' ,
dehumidifier _mode : '除湿模式' ,
fan _level : '风速档位' ,
uv : 'UV 杀菌' ,
brightness : '灯光亮度' ,
color _temp : '灯光色温' ,
volume : '音箱音量' ,
speaker _sleep _mode : '音箱睡眠模式' ,
mattress _left _heating : '床垫左侧加热' ,
mattress _right _heating : '床垫右侧加热' ,
mattress _left _mode : '床垫左侧承托模式' ,
mattress _right _mode : '床垫右侧承托模式'
}
function setFeedback ( type : 'success' | 'fail' , message : string ) {
actionFeedback . value = { type , message }
}
const operationBusy = computed ( ( ) =>
loading . value || Boolean ( sceneRunning . value ) || Boolean ( settingParam . value ) || Boolean ( switchingDeviceId . value )
)
const currentOperationText = computed ( ( ) => {
if ( sceneRunning . value === 'sleep' ) return '正在启动全屋舒睡模式…'
if ( sceneRunning . value === 'all-off' ) return '正在关闭场景设备…'
if ( switchingDeviceId . value ) return '正在同步设备开关状态…'
if ( settingParam . value ) {
const parameterType = Object . keys ( parameterLabels )
. find ( key => settingParam . value ? . endsWith ( ` _ ${ key } ` ) )
return ` 正在设置 ${ parameterLabels [ parameterType ? ? '' ] ? ? '设备参数' } … `
}
if ( loading . value ) return '正在刷新设备状态…'
return ''
} )
// 本地参数临时状态映射
const localParams = reactive ( {
@@ -38,6 +82,10 @@ const localParams = reactive({
mattressRightMode : '智能适配'
} )
function clamp ( val : number , min : number , max : number ) : number {
return Math . min ( max , Math . max ( min , val ) )
}
function numberValue ( value : unknown ) : number | undefined {
if ( typeof value === 'number' && Number . isFinite ( value ) ) return value
if ( typeof value === 'string' ) {
@@ -47,33 +95,42 @@ function numberValue(value: unknown): number | undefined {
return undefined
}
async function loadStatus ( ) {
async function loadStatus ( notify = false ) {
loading . value = true
try {
haStatus . value = await healthApi . homeAssistantStatus ( )
const controls = haStatus . value ? . controlParameters ? ? { }
const humidifier = controls . humidity
const humidity = numberValue ( humidifier ? . attributes ? . humidity )
if ( humidity !== undefined ) localParams . dehumidifierHumidity = humidity
if ( humidity !== undefined ) localParams . dehumidifierHumidity = clamp ( humidity , 40 , 70 )
if ( typeof humidifier ? . attributes ? . mode === 'string' ) localParams . dehumidifierMode = humidifier . attributes . mode
if ( controls . fan _level ? . state ) localParams . dehumidifierFan = controls . fan _level . state . replace ( '档' , '挡' )
localParams . dehumidifierUv = controls . uv ? . state === 'on'
const brightness = numberValue ( controls . brightness ? . attributes ? . brightness )
if ( brightness !== undefined ) localParams . lightBrightness = Math . round ( brightness / 255 * 100 )
if ( brightness !== undefined ) localParams . lightBrightness = clamp ( Math . round ( brightness / 255 * 100 ) , 1 , 100 )
const kelvin = numberValue ( controls . color _temp ? . attributes ? . color _temp _kelvin )
if ( kelvin !== undefined ) localParams . lightColorTemp = kelvin
if ( kelvin !== undefined ) localParams . lightColorTemp = clamp ( kelvin , 2200 , 6500 )
const volume = numberValue ( controls . volume ? . attributes ? . volume _level )
if ( volume !== undefined ) localParams . speakerVolume = Math . round ( volume * 100 )
if ( volume !== undefined ) localParams . speakerVolume = clamp ( Math . round ( volume * 100 ) , 0 , 100 )
localParams . speakerSleepMode = controls . speaker _sleep _mode ? . state === 'on'
localParams . mattressLeftHeating = numberValue ( controls . mattress _left _heating ? . state ) ? ? 1
localParams . mattressRightHeating = numberValue ( controls . mattress _right _heating ? . state ) ? ? 1
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
lastUpdatedAt . value = new Intl . DateTimeFormat ( 'zh-CN' , {
hour : '2-digit' , minute : '2-digit' , second : '2-digit' , hour12 : false
} ) . format ( new Date ( ) )
if ( notify ) {
setFeedback ( 'success' , '设备状态已刷新' )
showSuccessToast ( '设备状态已刷新' )
}
} catch ( err ) {
showToast ( err instanceof Error ? err . message : '获取设备状态失败' )
const message = err instanceof Error ? err . message : '获取设备状态失败'
setFeedback ( 'fail' , message )
showFailToast ( message )
} finally {
loading . value = false
}
@@ -107,31 +164,64 @@ function isDeviceTurnedOn(device?: HomeAssistantDiscoveredDevice): boolean {
if ( ! device || ! device . primaryControlEntityId ) return false
const target = device . entities . find ( e => e . entityId === device . primaryControlEntityId )
if ( ! target ) return false
if ( target . domain === 'media_player' ) {
const muted = target . attributes ? . is _volume _muted
if ( typeof muted === 'boolean' ) return ! muted
if ( typeof muted === 'string' ) return muted . toLowerCase ( ) !== 'true'
}
return target . state === 'on' || target . state === 'playing'
}
function updateEntityState ( deviceId : string , entityId : string , state : string , isMuted ? : boolean | null ) {
const device = haStatus . value ? . devices . find ( item => item . deviceId === deviceId )
const entity = device ? . entities . find ( item => item . entityId === entityId )
if ( ! entity ) return
entity . state = state
if ( typeof isMuted === 'boolean' ) {
entity . attributes = { ... ( entity . attributes ? ? { } ) , is _volume _muted : isMuted }
}
}
// 单独开关设备
async function toggleDevice ( device ? : HomeAssistantDiscoveredDevice ) {
if ( ! device || ! device . primaryControlEntityId ) {
showToast ( '该设备暂无可控主开关' )
const message = '该设备暂无可控主开关'
setFeedback ( 'fail' , message )
showFailToast ( message )
return
}
const nextOn = ! isDeviceTurnedOn ( device )
const isMediaDevice = device . category === 'Media'
switchingDeviceId . value = device . deviceId
const loadingToast = showLoadingToast ( {
message : isMediaDevice
? ( nextOn ? '正在取消静音…' : '正在静音…' )
: ( nextOn ? '正在开启设备…' : '正在关闭设备…' ) ,
forbidClick : true ,
duration : 0
} )
try {
const res = await healthApi . switchDevice ( {
deviceId : device . deviceId ,
turnOn : nextOn ,
entityId : device . primaryControlEntityId
} )
showToast ( {
type : res . success ? 'success ' : 'fail ' ,
message : res . message || ( nextOn ? '已开启' : '已关闭' )
} )
haStatus . value = await healthApi . homeAssistantStatus ( )
const message = res . message || ( isMediaDevice
? ( nextOn ? '音箱声音已开启 ' : '音箱已静音 ' )
: ( nextOn ? '设备 已开启' : '设备 已关闭' ) )
if ( ! res . success ) throw new Error ( message )
await loadStatus ( )
// 命令接口已经回读并确认真实状态;覆盖可能仍处于短暂缓存中的状态列表。
updateEntityState ( res . deviceId , res . entityId , res . state , res . isMuted )
setFeedback ( 'success' , message )
showSuccessToast ( message )
} catch ( err ) {
showToast ( err instanceof Error ? err . message : '控制失败' )
const message = err instanceof Error ? err . message : '控制失败'
await loadStatus ( )
setFeedback ( 'fail' , message )
showFailToast ( message )
} finally {
loadingToast . close ( )
switchingDeviceId . value = null
}
}
@@ -140,6 +230,12 @@ async function toggleDevice(device?: HomeAssistantDiscoveredDevice) {
async function applyParam ( deviceId : string , paramType : string , val : unknown , entityId ? : string ) {
const key = ` ${ deviceId } _ ${ paramType } `
settingParam . value = key
const label = parameterLabels [ paramType ] || '设备参数'
const loadingToast = showLoadingToast ( {
message : ` 正在设置 ${ label } … ` ,
forbidClick : true ,
duration : 0
} )
try {
const res = await healthApi . setParameter ( {
deviceId ,
@@ -147,34 +243,73 @@ async function applyParam(deviceId: string, paramType: string, val: unknown, ent
parameterType : paramType ,
value : val
} )
showToast ( {
type : res . success ? 'success' : 'fail' ,
message : res . message || '参数设置成功'
} )
const message = res . message || ` ${ label } 设置成功 `
if ( ! res . success ) throw new Error ( message )
await loadStatus ( )
setFeedback ( 'success' , message )
showSuccessToast ( message )
} catch ( err ) {
showToast ( err instanceof Error ? err . message : '参数设置失败' )
const message = err instanceof Error ? err . message : ` ${ label } 设置失败 `
await loadStatus ( )
setFeedback ( 'fail' , message )
showFailToast ( message )
} finally {
loadingToast . close ( )
settingParam . value = null
}
}
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 )
}
// 一键场景
async function executeScene ( scene : 'sleep' | 'all-off' ) {
sceneRunning . value = scene
const loadingToast = showLoadingToast ( {
message : scene === 'sleep' ? '正在启动舒睡场景…' : '正在关闭场景设备…' ,
forbidClick : true ,
duration : 0
} )
try {
const res = scene === 'sleep'
? await healthApi . executeSleepScene ( )
: await healthApi . executeAllOffScene ( )
showToast ( { type : res . success ? 'success' : 'fail' , message : res . message } )
haStatus . value = await healthApi . homeAssistantStatus ( )
if ( ! res . success ) throw new Error ( res . message || '场景执行失败' )
await loadStatus ( )
setFeedback ( 'success' , res . message )
showSuccessToast ( res . message )
} catch ( err ) {
showToast ( err instanceof Error ? err . message : '场景执行失败' )
const message = err instanceof Error ? err . message : '场景执行失败'
await loadStatus ( )
setFeedback ( 'fail' , message )
showFailToast ( message )
} finally {
loadingToast . close ( )
sceneRunning . value = undefined
}
}
function handleBack ( ) {
if ( window . history . length > 1 ) {
router . back ( )
} else {
router . replace ( '/dashboard' )
}
}
onMounted ( ( ) => {
loadStatus ( )
} )
@@ -184,14 +319,16 @@ onMounted(() => {
< div class = "control-page" >
<!-- 顶部返回条 -- >
< header class = "top-nav" >
< button class = "back-btn" @click ="router.b ack() " >
< button class = "back-btn" @click ="handleB ack" >
< svg viewBox = "0 0 24 24" width = "20" height = "20" fill = "none" stroke = "currentColor" stroke -width = " 2.5 " >
< polyline points = "15 18 9 12 15 6" / >
< / svg >
< span > 返回首页 < / span >
< / button >
< h2 > 智能设备详情与参数调节 < / h2 >
< button class = "refresh-btn" @click ="loadStatus" > 刷新 < / button >
< button class = "refresh-btn" :disabled = "operationBusy" @click ="loadStatus(true) " >
{{ loading ? ' 刷新中 … ' : ' 刷新 ' }}
< / button >
< / header >
< main class = "control-content" >
@@ -215,30 +352,45 @@ onMounted(() => {
< div class = "scene-actions-grid" >
< button
class = "scene-btn sleep-btn"
: disabled = "sceneRunning !== undefined "
:disabled = "operationBusy "
@click ="executeScene('sleep')"
>
< div class = "s-icon" > 🌙 < / div >
< div class = "s-text" >
< strong > 一键启动全屋舒睡模式 < / strong >
< strong > { { sceneRunning === 'sleep' ? '正在启动舒睡模式…' : ' 一键启动全屋舒睡模式' } } < / strong >
< small > 协同控制除湿 、 香氛 、 灯光 、 音箱与床垫 < / small >
< / div >
< / button >
< button
class = "scene-btn off-btn"
: disabled = "sceneRunning !== undefined "
:disabled = "operationBusy "
@click ="executeScene('all-off')"
>
< div class = "s-icon" > ⚪ < / div >
< div class = "s-text" >
< strong > 一键关闭设备 < / strong >
< strong > { { sceneRunning === 'all-off' ? '正在关闭设备…' : '一键关闭设备' } } < / strong >
< small > 安全待机休眠 < / small >
< / div >
< / button >
< / div >
< / section >
< div
class = "operation-status"
:class = "actionFeedback?.type"
role = "status"
aria -live = " polite "
>
< span class = "operation-message" >
< i v-if = "operationBusy" class="operation-spinner" aria-hidden="true" > < / i >
< i v-else-if = "actionFeedback?.type === 'success'" class="operation-result" aria-hidden="true" > ✓ < / i >
< i v-else-if = "actionFeedback?.type === 'fail'" class="operation-result" aria-hidden="true" > ! < / i >
{{ currentOperationText | | actionFeedback ? .message | | ' 操作完成后会自动刷新设备状态 ' }}
< / span >
< small v-if = "lastUpdatedAt" > 最后刷新 {{ lastUpdatedAt }} < / small >
< / div >
< ! - - 设备 1 : 米家变频净化除湿机 Max - - >
< section class = "device-card" v-if = "dehumidifierDevice" >
< div class = "device-head" >
@@ -255,6 +407,7 @@ onMounted(() => {
: type = "isDeviceTurnedOn(dehumidifierDevice) ? 'primary' : 'default'"
: color = "isDeviceTurnedOn(dehumidifierDevice) ? '#0c5045' : ''"
: loading = "switchingDeviceId === dehumidifierDevice.deviceId"
:disabled = "operationBusy"
@click ="toggleDevice(dehumidifierDevice)"
>
{ { isDeviceTurnedOn ( dehumidifierDevice ) ? '已开启' : '已关闭' } }
@@ -273,6 +426,7 @@ onMounted(() => {
:min = "40"
:max = "70"
:step = "1"
:disabled = "operationBusy"
active -color = " # 0c5045 "
@change ="applyParam(dehumidifierDevice.deviceId, 'humidity', localParams.dehumidifierHumidity)"
/ >
@@ -291,6 +445,7 @@ onMounted(() => {
v-for = "mode in ['睡眠', '智能']"
:key = "mode"
: class = "{ active: localParams.dehumidifierMode === mode }"
:disabled = "operationBusy"
@click ="localParams.dehumidifierMode = mode; applyParam(dehumidifierDevice.deviceId, 'dehumidifier_mode', mode)"
> { { mode } } < / button >
< / div >
@@ -304,6 +459,7 @@ onMounted(() => {
v-for = "fan in ['1挡', '2挡', '3挡']"
:key = "fan"
: class = "{ active: localParams.dehumidifierFan === fan }"
:disabled = "operationBusy"
@click ="localParams.dehumidifierFan = fan; applyParam(dehumidifierDevice.deviceId, 'fan_level', fan)"
>
{ { fan } }
@@ -320,6 +476,7 @@ onMounted(() => {
< button
class = "mini-toggle"
: class = "{ active: localParams.dehumidifierUv }"
:disabled = "operationBusy"
@click ="localParams.dehumidifierUv = !localParams.dehumidifierUv; applyParam(dehumidifierDevice.deviceId, 'uv', localParams.dehumidifierUv)"
>
{ { localParams . dehumidifierUv ? '已开启' : '已关闭' } }
@@ -344,6 +501,7 @@ onMounted(() => {
: type = "isDeviceTurnedOn(lightDevice) ? 'primary' : 'default'"
: color = "isDeviceTurnedOn(lightDevice) ? '#0c5045' : ''"
: loading = "switchingDeviceId === lightDevice.deviceId"
:disabled = "operationBusy"
@click ="toggleDevice(lightDevice)"
>
{ { isDeviceTurnedOn ( lightDevice ) ? '已点亮' : '已熄灭' } }
@@ -361,9 +519,15 @@ onMounted(() => {
v-model = "localParams.lightBrightness"
:min = "1"
:max = "100"
:disabled = "operationBusy"
active -color = " # d6a657 "
@change ="applyParam(lightDevice.deviceId, 'brightness', localParams.lightBrightness)"
/ >
< div class = "slider-scale" >
< span > 1 % 柔和微光 < / span >
< span > 50 % 舒缓 < / span >
< span > 100 % 满亮 < / span >
< / div >
< / div >
<!-- 色温滑块 -- >
@@ -375,14 +539,15 @@ onMounted(() => {
< VanSlider
v-model = "localParams.lightColorTemp"
:min = "2200"
:max = "50 00"
:max = "6 500"
:step = "50"
:disabled = "operationBusy"
active -color = " # c87f2b "
@change ="applyParam(lightDevice.deviceId, 'color_temp', localParams.lightColorTemp)"
/ >
< div class = "slider-scale" >
< span > 2200 K 超暖橙光 ( 利于褪黑素 ) < / span >
< span > 5000 K 明亮 白光< / span >
< span > 6500 K 清爽冷 白光< / span >
< / div >
< / div >
< / div >
@@ -404,9 +569,10 @@ onMounted(() => {
: type = "isDeviceTurnedOn(speakerDevice) ? 'primary' : 'default'"
: color = "isDeviceTurnedOn(speakerDevice) ? '#0c5045' : ''"
: loading = "switchingDeviceId === speakerDevice.deviceId"
:disabled = "operationBusy"
@click ="toggleDevice(speakerDevice)"
>
{ { isDeviceTurnedOn ( speakerDevice ) ? '正在播放 ' : '已暂停 ' } }
{ { isDeviceTurnedOn ( speakerDevice ) ? '声音开启 ' : '已静音 ' } }
< / VanButton >
< / div >
@@ -421,9 +587,15 @@ onMounted(() => {
v-model = "localParams.speakerVolume"
:min = "1"
:max = "100"
:disabled = "operationBusy"
active -color = " # 0c5045 "
@change ="applyParam(speakerDevice.deviceId, 'volume', localParams.speakerVolume)"
/ >
< div class = "slider-scale" >
< span > 1 % 轻柔助眠 < / span >
< span > 50 % 适中聆听 < / span >
< span > 100 % 满格音量 < / span >
< / div >
< / div >
< div class = "param-row inline-row" >
@@ -434,6 +606,7 @@ onMounted(() => {
< button
class = "mini-toggle"
: class = "{ active: localParams.speakerSleepMode }"
:disabled = "operationBusy"
@click ="localParams.speakerSleepMode = !localParams.speakerSleepMode; applyParam(speakerDevice.deviceId, 'speaker_sleep_mode', localParams.speakerSleepMode)"
>
{ { localParams . speakerSleepMode ? '已开启' : '已关闭' } }
@@ -458,6 +631,7 @@ onMounted(() => {
: type = "isDeviceTurnedOn(fragranceDevice) ? 'primary' : 'default'"
: color = "isDeviceTurnedOn(fragranceDevice) ? '#0c5045' : ''"
: loading = "switchingDeviceId === fragranceDevice.deviceId"
:disabled = "operationBusy"
@click ="toggleDevice(fragranceDevice)"
>
{ { isDeviceTurnedOn ( fragranceDevice ) ? '释放中' : '已关闭' } }
@@ -496,6 +670,7 @@ onMounted(() => {
:min = "0"
:max = "4"
:step = "1"
:disabled = "operationBusy"
active -color = " # d66927 "
@change ="applyParam(mattressDevice.deviceId, 'mattress_left_heating', localParams.mattressLeftHeating)"
/ >
@@ -517,9 +692,21 @@ onMounted(() => {
:min = "0"
:max = "4"
:step = "1"
:disabled = "operationBusy"
active -color = " # d66927 "
@change ="applyParam(mattressDevice.deviceId, 'mattress_right_heating', localParams.mattressRightHeating)"
/ >
< div class = "slider-scale" >
< span > 0 关闭 < / span >
< span > 2 舒适 < / span >
< span > 4 强加热 < / span >
< / div >
< / div >
< div class = "param-row" >
< p class = "mattress-mode-note" >
左右承托共用气泵 , 同一时间仅能运行一侧 ; 启动一侧后 , 另一侧会自动停止 。
< / p >
< / div >
< div class = "param-row" >
@@ -529,7 +716,8 @@ onMounted(() => {
v-for = "mode in ['无', '瑜伽助眠', '智能适配', '智能腰托']"
:key = "`left-${mode}`"
: class = "{ active: localParams.mattressLeftMode === mode }"
@click ="localParams.mattressLeftMode = mode; applyParam(mattressDevice.deviceId, 'mattress_left_mode', mode)"
:disabled = "operationBusy"
@click ="setMattressMode('left', mode)"
> { { mode } } < / button >
< / div >
< / div >
@@ -541,7 +729,8 @@ onMounted(() => {
v-for = "mode in ['无', '瑜伽助眠', '智能适配', '智能腰托']"
:key = "`right-${mode}`"
: class = "{ active: localParams.mattressRightMode === mode }"
@click ="localParams.mattressRightMode = mode; applyParam(mattressDevice.deviceId, 'mattress_right_mode', mode)"
:disabled = "operationBusy"
@click ="setMattressMode('right', mode)"
> { { mode } } < / button >
< / div >
< / div >
@@ -558,7 +747,9 @@ onMounted(() => {
{ { sensorDevice . online ? '实时采集正常' : '设备离线' } }
< / span >
< / div >
< button class = "text-refresh" @click ="loadStatus" > 手动刷新指标 < / button >
< button class = "text-refresh" :disabled = "operationBusy" @click ="loadStatus(true)" >
{{ loading ? ' 刷新中 … ' : ' 手动刷新指标 ' }}
< / button >
< / div >
< div class = "sensor-grid" >
@@ -613,6 +804,7 @@ onMounted(() => {
background : # f2f0e9 ;
min - height : 100 vh ;
padding - bottom : 40 px ;
overflow - x : hidden ;
}
. top - nav {
@@ -656,6 +848,12 @@ onMounted(() => {
cursor : pointer ;
}
. refresh - btn : disabled ,
. text - refresh : disabled {
opacity : 0.5 ;
cursor : not - allowed ;
}
. control - content {
padding : 14 px ;
}
@@ -716,6 +914,11 @@ onMounted(() => {
transition : all 0.2 s ;
}
. scene - btn : disabled {
opacity : 0.62 ;
cursor : wait ;
}
. sleep - btn {
background : rgba ( 214 , 166 , 87 , 0.2 ) ;
color : # fff ;
@@ -747,6 +950,72 @@ onMounted(() => {
margin - top : 2 px ;
}
. operation - status {
display : flex ;
align - items : center ;
justify - content : space - between ;
gap : 10 px ;
min - height : 38 px ;
padding : 9 px 12 px ;
margin : - 4 px 2 px 14 px ;
border : 1 px solid rgba ( 12 , 80 , 69 , 0.12 ) ;
border - radius : 12 px ;
background : rgba ( 255 , 255 , 255 , 0.72 ) ;
color : # 536 a62 ;
font - size : 11 px ;
}
. operation - status . success {
border - color : rgba ( 12 , 80 , 69 , 0.22 ) ;
background : # f0f8f5 ;
color : # 0 c5045 ;
}
. operation - status . fail {
border - color : rgba ( 176 , 72 , 56 , 0.24 ) ;
background : # fff4f1 ;
color : # 9 f3f32 ;
}
. operation - message {
display : inline - flex ;
align - items : center ;
min - width : 0 ;
gap : 7 px ;
font - weight : 600 ;
}
. operation - status small {
flex : none ;
color : # 86968 f ;
font - size : 10 px ;
}
. operation - result {
display : inline - grid ;
place - items : center ;
width : 17 px ;
height : 17 px ;
border - radius : 50 % ;
background : currentColor ;
color : # fff ;
font - size : 11 px ;
font - style : normal ;
}
. operation - spinner {
width : 15 px ;
height : 15 px ;
border : 2 px solid rgba ( 12 , 80 , 69 , 0.18 ) ;
border - top - color : # 0 c5045 ;
border - radius : 50 % ;
animation : operation - spin 0.75 s linear infinite ;
}
@ keyframes operation - spin {
to { transform : rotate ( 360 deg ) ; }
}
/* 设备卡片 */
. device - card {
background : # ffffff ;
@@ -803,13 +1072,13 @@ onMounted(() => {
. params - body {
display : flex ;
flex - direction : column ;
gap : 14 px ;
gap : 18 px ;
}
. param - row {
display : flex ;
flex - direction : column ;
gap : 6 px ;
position : relative ;
}
. param - row . inline - row {
@@ -822,18 +1091,21 @@ onMounted(() => {
display : flex ;
justify - content : space - between ;
align - items : center ;
margin - bottom : 12 px ;
}
. p - title {
font - size : 12 px ;
font - size : 13 px ;
color : # 556 c64 ;
font - weight : 600 ;
line - height : 1.2 ;
}
. p - val {
font - size : 14 px ;
color : # 0 c5045 ;
font - weight : 700 ;
line - height : 1.2 ;
}
. p - tip {
@@ -841,12 +1113,49 @@ onMounted(() => {
color : # 8 fa099 ;
}
. mattress - mode - note {
margin : 0 ;
padding : 9 px 11 px ;
border - radius : 9 px ;
background : # fff7e8 ;
color : # 8 a6428 ;
font - size : 11 px ;
line - height : 1.55 ;
}
/* 滑块样式:左右各预留 10px 间距,使滑块在 0% 和 100% 端点时,外凸的圆形按钮完整居中且不被裁剪 */
. param - row : deep ( . van - slider ) {
margin : 10 px 10 px 14 px 10 px ;
height : 4 px ;
border - radius : 2 px ;
background - color : # e6eae8 ;
box - sizing : border - box ;
}
. param - row : deep ( . van - slider _ _bar ) {
height : 4 px ;
border - radius : 2 px ;
}
. param - row : deep ( . van - slider _ _button - wrapper ) {
z - index : 2 ;
}
. param - row : deep ( . van - slider _ _button ) {
width : 20 px ;
height : 20 px ;
border - radius : 50 % ;
background - color : # ffffff ;
box - shadow : 0 2 px 6 px rgba ( 0 , 0 , 0 , 0.16 ) ;
border : 1 px solid rgba ( 0 , 0 , 0 , 0.05 ) ;
}
. slider - scale {
display : flex ;
justify - content : space - between ;
font - size : 10 px ;
color : # 8 d9e97 ;
margin - top : 4 px ;
padding : 0 2 px ;
}
. btn - group {
@@ -874,6 +1183,12 @@ onMounted(() => {
font - weight : 700 ;
}
. btn - group button : disabled ,
. mini - toggle : disabled {
opacity : 0.55 ;
cursor : not - allowed ;
}
. mini - toggle {
padding : 6 px 14 px ;
border - radius : 14 px ;