111
This commit is contained in:
+12
@@ -5,6 +5,18 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
<meta name="theme-color" content="#0c5045" />
|
<meta name="theme-color" content="#0c5045" />
|
||||||
<title>常青树健康管理</title>
|
<title>常青树健康管理</title>
|
||||||
|
<script>
|
||||||
|
function hideWxToolbar() {
|
||||||
|
if (typeof WeixinJSBridge !== 'undefined' && WeixinJSBridge.call) {
|
||||||
|
WeixinJSBridge.call('hideToolbar');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof WeixinJSBridge === 'undefined') {
|
||||||
|
document.addEventListener('WeixinJSBridgeReady', hideWxToolbar, false);
|
||||||
|
} else {
|
||||||
|
hideWxToolbar();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ const currentTab = computed(() => {
|
|||||||
|
|
||||||
function navigate(target: string) {
|
function navigate(target: string) {
|
||||||
if (target === 'home' && route.path !== '/dashboard') {
|
if (target === 'home' && route.path !== '/dashboard') {
|
||||||
router.push('/dashboard')
|
router.replace('/dashboard')
|
||||||
} else if (target === 'assessment' && route.path !== '/assessment') {
|
} else if (target === 'assessment' && route.path !== '/assessment') {
|
||||||
router.push('/assessment')
|
router.replace('/assessment')
|
||||||
} else if (target === 'profile' && route.path !== '/profile') {
|
} else if (target === 'profile' && route.path !== '/profile') {
|
||||||
router.push('/profile')
|
router.replace('/profile')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -80,15 +80,19 @@ function navigate(target: string) {
|
|||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
height: 60px;
|
min-height: 56px;
|
||||||
background: rgba(255, 255, 255, 0.96);
|
height: calc(56px + env(safe-area-inset-bottom, 0px));
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
border-top: 1px solid rgba(18, 63, 55, 0.08);
|
border-top: 1px solid rgba(18, 63, 55, 0.08);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
z-index: 99;
|
padding-top: 6px;
|
||||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
z-index: 999;
|
||||||
box-shadow: 0 -4px 16px rgba(18, 63, 55, 0.04);
|
box-shadow: 0 -4px 16px rgba(18, 63, 55, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,35 +102,33 @@ function navigate(target: string) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 44px;
|
||||||
color: #7d8e87;
|
color: #7d8e87;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-icon {
|
.tab-icon {
|
||||||
margin-bottom: 2px;
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-label {
|
.tab-label {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
|
margin-top: 2px;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item.active {
|
.tab-item.active {
|
||||||
color: #0c5045;
|
color: #0c5045;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item.active .tab-icon {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-item.active .tab-label {
|
.tab-item.active .tab-label {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,4 +24,10 @@ router.beforeEach((to) => {
|
|||||||
if (to.meta.guest && hasToken()) return '/dashboard'
|
if (to.meta.guest && hasToken()) return '/dashboard'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.afterEach(() => {
|
||||||
|
if (typeof (window as any).WeixinJSBridge !== 'undefined' && (window as any).WeixinJSBridge.call) {
|
||||||
|
(window as any).WeixinJSBridge.call('hideToolbar')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ export type HomeAssistantDeviceSwitchOutput = {
|
|||||||
deviceId: string
|
deviceId: string
|
||||||
entityId: string
|
entityId: string
|
||||||
state: string
|
state: string
|
||||||
|
isMuted?: boolean | null
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -11,7 +11,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
body { margin: 0; min-width: 320px; min-height: 100vh; background: #e8e6df; }
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #e8e6df;
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
#app {
|
#app {
|
||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -19,6 +27,7 @@ body { margin: 0; min-width: 320px; min-height: 100vh; background: #e8e6df; }
|
|||||||
position: relative;
|
position: relative;
|
||||||
background: #f2f0e9;
|
background: #f2f0e9;
|
||||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.08);
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
button, input, textarea { font: inherit; }
|
button, input, textarea { font: inherit; }
|
||||||
button { cursor: pointer; }
|
button { cursor: pointer; }
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ async function handleSave() {
|
|||||||
answers: { ...answers }
|
answers: { ...answers }
|
||||||
})
|
})
|
||||||
showToast({ type: 'success', message: '评估已完成,已更新智能设备方案' })
|
showToast({ type: 'success', message: '评估已完成,已更新智能设备方案' })
|
||||||
router.push('/dashboard')
|
router.replace('/dashboard')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err instanceof Error ? err.message : '保存失败,请重试')
|
showToast(err instanceof Error ? err.message : '保存失败,请重试')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -88,14 +88,14 @@ onMounted(load)
|
|||||||
round
|
round
|
||||||
color="#f1c37a"
|
color="#f1c37a"
|
||||||
text-color="#183b34"
|
text-color="#183b34"
|
||||||
@click="router.push('/assessment')"
|
@click="router.replace('/assessment')"
|
||||||
>
|
>
|
||||||
去健康评估
|
去健康评估
|
||||||
</van-button>
|
</van-button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 未完成评估提示横幅 -->
|
<!-- 未完成评估提示横幅 -->
|
||||||
<section v-if="!dashboard?.hasCompletedAssessment" class="assessment-guide-banner" @click="router.push('/assessment')">
|
<section v-if="!dashboard?.hasCompletedAssessment" class="assessment-guide-banner" @click="router.replace('/assessment')">
|
||||||
<div class="guide-badge">重要提醒</div>
|
<div class="guide-badge">重要提醒</div>
|
||||||
<div class="guide-body">
|
<div class="guide-body">
|
||||||
<div class="guide-text">
|
<div class="guide-text">
|
||||||
|
|||||||
+355
-40
@@ -1,7 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
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 {
|
import {
|
||||||
healthApi,
|
healthApi,
|
||||||
type HomeAssistantDiscoveredDevice,
|
type HomeAssistantDiscoveredDevice,
|
||||||
@@ -14,6 +20,44 @@ const haStatus = ref<HomeAssistantStatus>()
|
|||||||
const sceneRunning = ref<'sleep' | 'all-off'>()
|
const sceneRunning = ref<'sleep' | 'all-off'>()
|
||||||
const settingParam = ref<string | null>(null)
|
const settingParam = ref<string | null>(null)
|
||||||
const switchingDeviceId = 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({
|
const localParams = reactive({
|
||||||
@@ -38,6 +82,10 @@ const localParams = reactive({
|
|||||||
mattressRightMode: '智能适配'
|
mattressRightMode: '智能适配'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function clamp(val: number, min: number, max: number): number {
|
||||||
|
return Math.min(max, Math.max(min, val))
|
||||||
|
}
|
||||||
|
|
||||||
function numberValue(value: unknown): number | undefined {
|
function numberValue(value: unknown): number | undefined {
|
||||||
if (typeof value === 'number' && Number.isFinite(value)) return value
|
if (typeof value === 'number' && Number.isFinite(value)) return value
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
@@ -47,33 +95,42 @@ function numberValue(value: unknown): number | undefined {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadStatus() {
|
async function loadStatus(notify = false) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
haStatus.value = await healthApi.homeAssistantStatus()
|
haStatus.value = await healthApi.homeAssistantStatus()
|
||||||
const controls = haStatus.value?.controlParameters ?? {}
|
const controls = haStatus.value?.controlParameters ?? {}
|
||||||
const humidifier = controls.humidity
|
const humidifier = controls.humidity
|
||||||
const humidity = numberValue(humidifier?.attributes?.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 (typeof humidifier?.attributes?.mode === 'string') localParams.dehumidifierMode = humidifier.attributes.mode
|
||||||
if (controls.fan_level?.state) localParams.dehumidifierFan = controls.fan_level.state.replace('档', '挡')
|
if (controls.fan_level?.state) localParams.dehumidifierFan = controls.fan_level.state.replace('档', '挡')
|
||||||
localParams.dehumidifierUv = controls.uv?.state === 'on'
|
localParams.dehumidifierUv = controls.uv?.state === 'on'
|
||||||
|
|
||||||
const brightness = numberValue(controls.brightness?.attributes?.brightness)
|
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)
|
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)
|
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.speakerSleepMode = controls.speaker_sleep_mode?.state === 'on'
|
||||||
|
|
||||||
localParams.mattressLeftHeating = numberValue(controls.mattress_left_heating?.state) ?? 1
|
localParams.mattressLeftHeating = clamp(numberValue(controls.mattress_left_heating?.state) ?? 1, 0, 4)
|
||||||
localParams.mattressRightHeating = numberValue(controls.mattress_right_heating?.state) ?? 1
|
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
|
||||||
|
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) {
|
} catch (err) {
|
||||||
showToast(err instanceof Error ? err.message : '获取设备状态失败')
|
const message = err instanceof Error ? err.message : '获取设备状态失败'
|
||||||
|
setFeedback('fail', message)
|
||||||
|
showFailToast(message)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -107,31 +164,64 @@ function isDeviceTurnedOn(device?: HomeAssistantDiscoveredDevice): boolean {
|
|||||||
if (!device || !device.primaryControlEntityId) return false
|
if (!device || !device.primaryControlEntityId) return false
|
||||||
const target = device.entities.find(e => e.entityId === device.primaryControlEntityId)
|
const target = device.entities.find(e => e.entityId === device.primaryControlEntityId)
|
||||||
if (!target) return false
|
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'
|
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) {
|
async function toggleDevice(device?: HomeAssistantDiscoveredDevice) {
|
||||||
if (!device || !device.primaryControlEntityId) {
|
if (!device || !device.primaryControlEntityId) {
|
||||||
showToast('该设备暂无可控主开关')
|
const message = '该设备暂无可控主开关'
|
||||||
|
setFeedback('fail', message)
|
||||||
|
showFailToast(message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const nextOn = !isDeviceTurnedOn(device)
|
const nextOn = !isDeviceTurnedOn(device)
|
||||||
|
const isMediaDevice = device.category === 'Media'
|
||||||
switchingDeviceId.value = device.deviceId
|
switchingDeviceId.value = device.deviceId
|
||||||
|
const loadingToast = showLoadingToast({
|
||||||
|
message: isMediaDevice
|
||||||
|
? (nextOn ? '正在取消静音…' : '正在静音…')
|
||||||
|
: (nextOn ? '正在开启设备…' : '正在关闭设备…'),
|
||||||
|
forbidClick: true,
|
||||||
|
duration: 0
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
const res = await healthApi.switchDevice({
|
const res = await healthApi.switchDevice({
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
turnOn: nextOn,
|
turnOn: nextOn,
|
||||||
entityId: device.primaryControlEntityId
|
entityId: device.primaryControlEntityId
|
||||||
})
|
})
|
||||||
showToast({
|
const message = res.message || (isMediaDevice
|
||||||
type: res.success ? 'success' : 'fail',
|
? (nextOn ? '音箱声音已开启' : '音箱已静音')
|
||||||
message: res.message || (nextOn ? '已开启' : '已关闭')
|
: (nextOn ? '设备已开启' : '设备已关闭'))
|
||||||
})
|
if (!res.success) throw new Error(message)
|
||||||
haStatus.value = await healthApi.homeAssistantStatus()
|
await loadStatus()
|
||||||
|
// 命令接口已经回读并确认真实状态;覆盖可能仍处于短暂缓存中的状态列表。
|
||||||
|
updateEntityState(res.deviceId, res.entityId, res.state, res.isMuted)
|
||||||
|
setFeedback('success', message)
|
||||||
|
showSuccessToast(message)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err instanceof Error ? err.message : '控制失败')
|
const message = err instanceof Error ? err.message : '控制失败'
|
||||||
|
await loadStatus()
|
||||||
|
setFeedback('fail', message)
|
||||||
|
showFailToast(message)
|
||||||
} finally {
|
} finally {
|
||||||
|
loadingToast.close()
|
||||||
switchingDeviceId.value = null
|
switchingDeviceId.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,6 +230,12 @@ async function toggleDevice(device?: HomeAssistantDiscoveredDevice) {
|
|||||||
async function applyParam(deviceId: string, paramType: string, val: unknown, entityId?: string) {
|
async function applyParam(deviceId: string, paramType: string, val: unknown, entityId?: string) {
|
||||||
const key = `${deviceId}_${paramType}`
|
const key = `${deviceId}_${paramType}`
|
||||||
settingParam.value = key
|
settingParam.value = key
|
||||||
|
const label = parameterLabels[paramType] || '设备参数'
|
||||||
|
const loadingToast = showLoadingToast({
|
||||||
|
message: `正在设置${label}…`,
|
||||||
|
forbidClick: true,
|
||||||
|
duration: 0
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
const res = await healthApi.setParameter({
|
const res = await healthApi.setParameter({
|
||||||
deviceId,
|
deviceId,
|
||||||
@@ -147,34 +243,73 @@ async function applyParam(deviceId: string, paramType: string, val: unknown, ent
|
|||||||
parameterType: paramType,
|
parameterType: paramType,
|
||||||
value: val
|
value: val
|
||||||
})
|
})
|
||||||
showToast({
|
const message = res.message || `${label}设置成功`
|
||||||
type: res.success ? 'success' : 'fail',
|
if (!res.success) throw new Error(message)
|
||||||
message: res.message || '参数设置成功'
|
|
||||||
})
|
|
||||||
await loadStatus()
|
await loadStatus()
|
||||||
|
setFeedback('success', message)
|
||||||
|
showSuccessToast(message)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err instanceof Error ? err.message : '参数设置失败')
|
const message = err instanceof Error ? err.message : `${label}设置失败`
|
||||||
|
await loadStatus()
|
||||||
|
setFeedback('fail', message)
|
||||||
|
showFailToast(message)
|
||||||
} finally {
|
} finally {
|
||||||
|
loadingToast.close()
|
||||||
settingParam.value = null
|
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') {
|
async function executeScene(scene: 'sleep' | 'all-off') {
|
||||||
sceneRunning.value = scene
|
sceneRunning.value = scene
|
||||||
|
const loadingToast = showLoadingToast({
|
||||||
|
message: scene === 'sleep' ? '正在启动舒睡场景…' : '正在关闭场景设备…',
|
||||||
|
forbidClick: true,
|
||||||
|
duration: 0
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
const res = scene === 'sleep'
|
const res = scene === 'sleep'
|
||||||
? await healthApi.executeSleepScene()
|
? await healthApi.executeSleepScene()
|
||||||
: await healthApi.executeAllOffScene()
|
: await healthApi.executeAllOffScene()
|
||||||
showToast({ type: res.success ? 'success' : 'fail', message: res.message })
|
if (!res.success) throw new Error(res.message || '场景执行失败')
|
||||||
haStatus.value = await healthApi.homeAssistantStatus()
|
await loadStatus()
|
||||||
|
setFeedback('success', res.message)
|
||||||
|
showSuccessToast(res.message)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err instanceof Error ? err.message : '场景执行失败')
|
const message = err instanceof Error ? err.message : '场景执行失败'
|
||||||
|
await loadStatus()
|
||||||
|
setFeedback('fail', message)
|
||||||
|
showFailToast(message)
|
||||||
} finally {
|
} finally {
|
||||||
|
loadingToast.close()
|
||||||
sceneRunning.value = undefined
|
sceneRunning.value = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
router.back()
|
||||||
|
} else {
|
||||||
|
router.replace('/dashboard')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadStatus()
|
loadStatus()
|
||||||
})
|
})
|
||||||
@@ -184,14 +319,16 @@ onMounted(() => {
|
|||||||
<div class="control-page">
|
<div class="control-page">
|
||||||
<!-- 顶部返回条 -->
|
<!-- 顶部返回条 -->
|
||||||
<header class="top-nav">
|
<header class="top-nav">
|
||||||
<button class="back-btn" @click="router.back()">
|
<button class="back-btn" @click="handleBack">
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5">
|
<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" />
|
<polyline points="15 18 9 12 15 6" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>返回首页</span>
|
<span>返回首页</span>
|
||||||
</button>
|
</button>
|
||||||
<h2>智能设备详情与参数调节</h2>
|
<h2>智能设备详情与参数调节</h2>
|
||||||
<button class="refresh-btn" @click="loadStatus">刷新</button>
|
<button class="refresh-btn" :disabled="operationBusy" @click="loadStatus(true)">
|
||||||
|
{{ loading ? '刷新中…' : '刷新' }}
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="control-content">
|
<main class="control-content">
|
||||||
@@ -215,30 +352,45 @@ onMounted(() => {
|
|||||||
<div class="scene-actions-grid">
|
<div class="scene-actions-grid">
|
||||||
<button
|
<button
|
||||||
class="scene-btn sleep-btn"
|
class="scene-btn sleep-btn"
|
||||||
:disabled="sceneRunning !== undefined"
|
:disabled="operationBusy"
|
||||||
@click="executeScene('sleep')"
|
@click="executeScene('sleep')"
|
||||||
>
|
>
|
||||||
<div class="s-icon">🌙</div>
|
<div class="s-icon">🌙</div>
|
||||||
<div class="s-text">
|
<div class="s-text">
|
||||||
<strong>一键启动全屋舒睡模式</strong>
|
<strong>{{ sceneRunning === 'sleep' ? '正在启动舒睡模式…' : '一键启动全屋舒睡模式' }}</strong>
|
||||||
<small>协同控制除湿、香氛、灯光、音箱与床垫</small>
|
<small>协同控制除湿、香氛、灯光、音箱与床垫</small>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="scene-btn off-btn"
|
class="scene-btn off-btn"
|
||||||
:disabled="sceneRunning !== undefined"
|
:disabled="operationBusy"
|
||||||
@click="executeScene('all-off')"
|
@click="executeScene('all-off')"
|
||||||
>
|
>
|
||||||
<div class="s-icon">⚪</div>
|
<div class="s-icon">⚪</div>
|
||||||
<div class="s-text">
|
<div class="s-text">
|
||||||
<strong>一键关闭设备</strong>
|
<strong>{{ sceneRunning === 'all-off' ? '正在关闭设备…' : '一键关闭设备' }}</strong>
|
||||||
<small>安全待机休眠</small>
|
<small>安全待机休眠</small>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 -->
|
<!-- 设备 1:米家变频净化除湿机 Max -->
|
||||||
<section class="device-card" v-if="dehumidifierDevice">
|
<section class="device-card" v-if="dehumidifierDevice">
|
||||||
<div class="device-head">
|
<div class="device-head">
|
||||||
@@ -255,6 +407,7 @@ onMounted(() => {
|
|||||||
:type="isDeviceTurnedOn(dehumidifierDevice) ? 'primary' : 'default'"
|
:type="isDeviceTurnedOn(dehumidifierDevice) ? 'primary' : 'default'"
|
||||||
:color="isDeviceTurnedOn(dehumidifierDevice) ? '#0c5045' : ''"
|
:color="isDeviceTurnedOn(dehumidifierDevice) ? '#0c5045' : ''"
|
||||||
:loading="switchingDeviceId === dehumidifierDevice.deviceId"
|
:loading="switchingDeviceId === dehumidifierDevice.deviceId"
|
||||||
|
:disabled="operationBusy"
|
||||||
@click="toggleDevice(dehumidifierDevice)"
|
@click="toggleDevice(dehumidifierDevice)"
|
||||||
>
|
>
|
||||||
{{ isDeviceTurnedOn(dehumidifierDevice) ? '已开启' : '已关闭' }}
|
{{ isDeviceTurnedOn(dehumidifierDevice) ? '已开启' : '已关闭' }}
|
||||||
@@ -273,6 +426,7 @@ onMounted(() => {
|
|||||||
:min="40"
|
:min="40"
|
||||||
:max="70"
|
:max="70"
|
||||||
:step="1"
|
:step="1"
|
||||||
|
:disabled="operationBusy"
|
||||||
active-color="#0c5045"
|
active-color="#0c5045"
|
||||||
@change="applyParam(dehumidifierDevice.deviceId, 'humidity', localParams.dehumidifierHumidity)"
|
@change="applyParam(dehumidifierDevice.deviceId, 'humidity', localParams.dehumidifierHumidity)"
|
||||||
/>
|
/>
|
||||||
@@ -291,6 +445,7 @@ onMounted(() => {
|
|||||||
v-for="mode in ['睡眠', '智能']"
|
v-for="mode in ['睡眠', '智能']"
|
||||||
:key="mode"
|
:key="mode"
|
||||||
:class="{ active: localParams.dehumidifierMode === mode }"
|
:class="{ active: localParams.dehumidifierMode === mode }"
|
||||||
|
:disabled="operationBusy"
|
||||||
@click="localParams.dehumidifierMode = mode; applyParam(dehumidifierDevice.deviceId, 'dehumidifier_mode', mode)"
|
@click="localParams.dehumidifierMode = mode; applyParam(dehumidifierDevice.deviceId, 'dehumidifier_mode', mode)"
|
||||||
>{{ mode }}</button>
|
>{{ mode }}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -304,6 +459,7 @@ onMounted(() => {
|
|||||||
v-for="fan in ['1挡', '2挡', '3挡']"
|
v-for="fan in ['1挡', '2挡', '3挡']"
|
||||||
:key="fan"
|
:key="fan"
|
||||||
:class="{ active: localParams.dehumidifierFan === fan }"
|
:class="{ active: localParams.dehumidifierFan === fan }"
|
||||||
|
:disabled="operationBusy"
|
||||||
@click="localParams.dehumidifierFan = fan; applyParam(dehumidifierDevice.deviceId, 'fan_level', fan)"
|
@click="localParams.dehumidifierFan = fan; applyParam(dehumidifierDevice.deviceId, 'fan_level', fan)"
|
||||||
>
|
>
|
||||||
{{ fan }}
|
{{ fan }}
|
||||||
@@ -320,6 +476,7 @@ onMounted(() => {
|
|||||||
<button
|
<button
|
||||||
class="mini-toggle"
|
class="mini-toggle"
|
||||||
:class="{ active: localParams.dehumidifierUv }"
|
:class="{ active: localParams.dehumidifierUv }"
|
||||||
|
:disabled="operationBusy"
|
||||||
@click="localParams.dehumidifierUv = !localParams.dehumidifierUv; applyParam(dehumidifierDevice.deviceId, 'uv', localParams.dehumidifierUv)"
|
@click="localParams.dehumidifierUv = !localParams.dehumidifierUv; applyParam(dehumidifierDevice.deviceId, 'uv', localParams.dehumidifierUv)"
|
||||||
>
|
>
|
||||||
{{ localParams.dehumidifierUv ? '已开启' : '已关闭' }}
|
{{ localParams.dehumidifierUv ? '已开启' : '已关闭' }}
|
||||||
@@ -344,6 +501,7 @@ onMounted(() => {
|
|||||||
:type="isDeviceTurnedOn(lightDevice) ? 'primary' : 'default'"
|
:type="isDeviceTurnedOn(lightDevice) ? 'primary' : 'default'"
|
||||||
:color="isDeviceTurnedOn(lightDevice) ? '#0c5045' : ''"
|
:color="isDeviceTurnedOn(lightDevice) ? '#0c5045' : ''"
|
||||||
:loading="switchingDeviceId === lightDevice.deviceId"
|
:loading="switchingDeviceId === lightDevice.deviceId"
|
||||||
|
:disabled="operationBusy"
|
||||||
@click="toggleDevice(lightDevice)"
|
@click="toggleDevice(lightDevice)"
|
||||||
>
|
>
|
||||||
{{ isDeviceTurnedOn(lightDevice) ? '已点亮' : '已熄灭' }}
|
{{ isDeviceTurnedOn(lightDevice) ? '已点亮' : '已熄灭' }}
|
||||||
@@ -361,9 +519,15 @@ onMounted(() => {
|
|||||||
v-model="localParams.lightBrightness"
|
v-model="localParams.lightBrightness"
|
||||||
:min="1"
|
:min="1"
|
||||||
:max="100"
|
:max="100"
|
||||||
|
:disabled="operationBusy"
|
||||||
active-color="#d6a657"
|
active-color="#d6a657"
|
||||||
@change="applyParam(lightDevice.deviceId, 'brightness', localParams.lightBrightness)"
|
@change="applyParam(lightDevice.deviceId, 'brightness', localParams.lightBrightness)"
|
||||||
/>
|
/>
|
||||||
|
<div class="slider-scale">
|
||||||
|
<span>1% 柔和微光</span>
|
||||||
|
<span>50% 舒缓</span>
|
||||||
|
<span>100% 满亮</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 色温滑块 -->
|
<!-- 色温滑块 -->
|
||||||
@@ -375,14 +539,15 @@ onMounted(() => {
|
|||||||
<VanSlider
|
<VanSlider
|
||||||
v-model="localParams.lightColorTemp"
|
v-model="localParams.lightColorTemp"
|
||||||
:min="2200"
|
:min="2200"
|
||||||
:max="5000"
|
:max="6500"
|
||||||
:step="50"
|
:step="50"
|
||||||
|
:disabled="operationBusy"
|
||||||
active-color="#c87f2b"
|
active-color="#c87f2b"
|
||||||
@change="applyParam(lightDevice.deviceId, 'color_temp', localParams.lightColorTemp)"
|
@change="applyParam(lightDevice.deviceId, 'color_temp', localParams.lightColorTemp)"
|
||||||
/>
|
/>
|
||||||
<div class="slider-scale">
|
<div class="slider-scale">
|
||||||
<span>2200K 超暖橙光 (利于褪黑素)</span>
|
<span>2200K 超暖橙光 (利于褪黑素)</span>
|
||||||
<span>5000K 明亮白光</span>
|
<span>6500K 清爽冷白光</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -404,9 +569,10 @@ onMounted(() => {
|
|||||||
:type="isDeviceTurnedOn(speakerDevice) ? 'primary' : 'default'"
|
:type="isDeviceTurnedOn(speakerDevice) ? 'primary' : 'default'"
|
||||||
:color="isDeviceTurnedOn(speakerDevice) ? '#0c5045' : ''"
|
:color="isDeviceTurnedOn(speakerDevice) ? '#0c5045' : ''"
|
||||||
:loading="switchingDeviceId === speakerDevice.deviceId"
|
:loading="switchingDeviceId === speakerDevice.deviceId"
|
||||||
|
:disabled="operationBusy"
|
||||||
@click="toggleDevice(speakerDevice)"
|
@click="toggleDevice(speakerDevice)"
|
||||||
>
|
>
|
||||||
{{ isDeviceTurnedOn(speakerDevice) ? '正在播放' : '已暂停' }}
|
{{ isDeviceTurnedOn(speakerDevice) ? '声音开启' : '已静音' }}
|
||||||
</VanButton>
|
</VanButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -421,9 +587,15 @@ onMounted(() => {
|
|||||||
v-model="localParams.speakerVolume"
|
v-model="localParams.speakerVolume"
|
||||||
:min="1"
|
:min="1"
|
||||||
:max="100"
|
:max="100"
|
||||||
|
:disabled="operationBusy"
|
||||||
active-color="#0c5045"
|
active-color="#0c5045"
|
||||||
@change="applyParam(speakerDevice.deviceId, 'volume', localParams.speakerVolume)"
|
@change="applyParam(speakerDevice.deviceId, 'volume', localParams.speakerVolume)"
|
||||||
/>
|
/>
|
||||||
|
<div class="slider-scale">
|
||||||
|
<span>1% 轻柔助眠</span>
|
||||||
|
<span>50% 适中聆听</span>
|
||||||
|
<span>100% 满格音量</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="param-row inline-row">
|
<div class="param-row inline-row">
|
||||||
@@ -434,6 +606,7 @@ onMounted(() => {
|
|||||||
<button
|
<button
|
||||||
class="mini-toggle"
|
class="mini-toggle"
|
||||||
:class="{ active: localParams.speakerSleepMode }"
|
:class="{ active: localParams.speakerSleepMode }"
|
||||||
|
:disabled="operationBusy"
|
||||||
@click="localParams.speakerSleepMode = !localParams.speakerSleepMode; applyParam(speakerDevice.deviceId, 'speaker_sleep_mode', localParams.speakerSleepMode)"
|
@click="localParams.speakerSleepMode = !localParams.speakerSleepMode; applyParam(speakerDevice.deviceId, 'speaker_sleep_mode', localParams.speakerSleepMode)"
|
||||||
>
|
>
|
||||||
{{ localParams.speakerSleepMode ? '已开启' : '已关闭' }}
|
{{ localParams.speakerSleepMode ? '已开启' : '已关闭' }}
|
||||||
@@ -458,6 +631,7 @@ onMounted(() => {
|
|||||||
:type="isDeviceTurnedOn(fragranceDevice) ? 'primary' : 'default'"
|
:type="isDeviceTurnedOn(fragranceDevice) ? 'primary' : 'default'"
|
||||||
:color="isDeviceTurnedOn(fragranceDevice) ? '#0c5045' : ''"
|
:color="isDeviceTurnedOn(fragranceDevice) ? '#0c5045' : ''"
|
||||||
:loading="switchingDeviceId === fragranceDevice.deviceId"
|
:loading="switchingDeviceId === fragranceDevice.deviceId"
|
||||||
|
:disabled="operationBusy"
|
||||||
@click="toggleDevice(fragranceDevice)"
|
@click="toggleDevice(fragranceDevice)"
|
||||||
>
|
>
|
||||||
{{ isDeviceTurnedOn(fragranceDevice) ? '释放中' : '已关闭' }}
|
{{ isDeviceTurnedOn(fragranceDevice) ? '释放中' : '已关闭' }}
|
||||||
@@ -496,6 +670,7 @@ onMounted(() => {
|
|||||||
:min="0"
|
:min="0"
|
||||||
:max="4"
|
:max="4"
|
||||||
:step="1"
|
:step="1"
|
||||||
|
:disabled="operationBusy"
|
||||||
active-color="#d66927"
|
active-color="#d66927"
|
||||||
@change="applyParam(mattressDevice.deviceId, 'mattress_left_heating', localParams.mattressLeftHeating)"
|
@change="applyParam(mattressDevice.deviceId, 'mattress_left_heating', localParams.mattressLeftHeating)"
|
||||||
/>
|
/>
|
||||||
@@ -517,9 +692,21 @@ onMounted(() => {
|
|||||||
:min="0"
|
:min="0"
|
||||||
:max="4"
|
:max="4"
|
||||||
:step="1"
|
:step="1"
|
||||||
|
:disabled="operationBusy"
|
||||||
active-color="#d66927"
|
active-color="#d66927"
|
||||||
@change="applyParam(mattressDevice.deviceId, 'mattress_right_heating', localParams.mattressRightHeating)"
|
@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>
|
||||||
|
|
||||||
<div class="param-row">
|
<div class="param-row">
|
||||||
@@ -529,7 +716,8 @@ onMounted(() => {
|
|||||||
v-for="mode in ['无', '瑜伽助眠', '智能适配', '智能腰托']"
|
v-for="mode in ['无', '瑜伽助眠', '智能适配', '智能腰托']"
|
||||||
:key="`left-${mode}`"
|
:key="`left-${mode}`"
|
||||||
:class="{ active: localParams.mattressLeftMode === 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>
|
>{{ mode }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -541,7 +729,8 @@ onMounted(() => {
|
|||||||
v-for="mode in ['无', '瑜伽助眠', '智能适配', '智能腰托']"
|
v-for="mode in ['无', '瑜伽助眠', '智能适配', '智能腰托']"
|
||||||
:key="`right-${mode}`"
|
:key="`right-${mode}`"
|
||||||
:class="{ active: localParams.mattressRightMode === 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>
|
>{{ mode }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -558,7 +747,9 @@ onMounted(() => {
|
|||||||
{{ sensorDevice.online ? '实时采集正常' : '设备离线' }}
|
{{ sensorDevice.online ? '实时采集正常' : '设备离线' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="text-refresh" @click="loadStatus">手动刷新指标</button>
|
<button class="text-refresh" :disabled="operationBusy" @click="loadStatus(true)">
|
||||||
|
{{ loading ? '刷新中…' : '手动刷新指标' }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sensor-grid">
|
<div class="sensor-grid">
|
||||||
@@ -613,6 +804,7 @@ onMounted(() => {
|
|||||||
background: #f2f0e9;
|
background: #f2f0e9;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding-bottom: 40px;
|
padding-bottom: 40px;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-nav {
|
.top-nav {
|
||||||
@@ -656,6 +848,12 @@ onMounted(() => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.refresh-btn:disabled,
|
||||||
|
.text-refresh:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.control-content {
|
.control-content {
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
@@ -716,6 +914,11 @@ onMounted(() => {
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scene-btn:disabled {
|
||||||
|
opacity: 0.62;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
.sleep-btn {
|
.sleep-btn {
|
||||||
background: rgba(214, 166, 87, 0.2);
|
background: rgba(214, 166, 87, 0.2);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -747,6 +950,72 @@ onMounted(() => {
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.operation-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
margin: -4px 2px 14px;
|
||||||
|
border: 1px solid rgba(12, 80, 69, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
color: #536a62;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-status.success {
|
||||||
|
border-color: rgba(12, 80, 69, 0.22);
|
||||||
|
background: #f0f8f5;
|
||||||
|
color: #0c5045;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-status.fail {
|
||||||
|
border-color: rgba(176, 72, 56, 0.24);
|
||||||
|
background: #fff4f1;
|
||||||
|
color: #9f3f32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-message {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 7px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-status small {
|
||||||
|
flex: none;
|
||||||
|
color: #86968f;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-result {
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-spinner {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border: 2px solid rgba(12, 80, 69, 0.18);
|
||||||
|
border-top-color: #0c5045;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: operation-spin 0.75s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes operation-spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
/* 设备卡片 */
|
/* 设备卡片 */
|
||||||
.device-card {
|
.device-card {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
@@ -803,13 +1072,13 @@ onMounted(() => {
|
|||||||
.params-body {
|
.params-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.param-row {
|
.param-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.param-row.inline-row {
|
.param-row.inline-row {
|
||||||
@@ -822,18 +1091,21 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-title {
|
.p-title {
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
color: #556c64;
|
color: #556c64;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-val {
|
.p-val {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #0c5045;
|
color: #0c5045;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-tip {
|
.p-tip {
|
||||||
@@ -841,12 +1113,49 @@ onMounted(() => {
|
|||||||
color: #8fa099;
|
color: #8fa099;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mattress-mode-note {
|
||||||
|
margin: 0;
|
||||||
|
padding: 9px 11px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #fff7e8;
|
||||||
|
color: #8a6428;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滑块样式:左右各预留 10px 间距,使滑块在 0% 和 100% 端点时,外凸的圆形按钮完整居中且不被裁剪 */
|
||||||
|
.param-row :deep(.van-slider) {
|
||||||
|
margin: 10px 10px 14px 10px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: #e6eae8;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-row :deep(.van-slider__bar) {
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-row :deep(.van-slider__button-wrapper) {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-row :deep(.van-slider__button) {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #ffffff;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.16);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
.slider-scale {
|
.slider-scale {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #8d9e97;
|
color: #8d9e97;
|
||||||
margin-top: 4px;
|
padding: 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-group {
|
.btn-group {
|
||||||
@@ -874,6 +1183,12 @@ onMounted(() => {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-group button:disabled,
|
||||||
|
.mini-toggle:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.mini-toggle {
|
.mini-toggle {
|
||||||
padding: 6px 14px;
|
padding: 6px 14px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ onMounted(() => {
|
|||||||
<section class="profile-card">
|
<section class="profile-card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<h3>身体基线档案</h3>
|
<h3>身体基线档案</h3>
|
||||||
<button class="edit-btn" @click="router.push('/assessment')">编辑指标</button>
|
<button class="edit-btn" @click="router.replace('/assessment')">编辑指标</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="metrics-grid">
|
<div class="metrics-grid">
|
||||||
@@ -180,7 +180,7 @@ onMounted(() => {
|
|||||||
size="small"
|
size="small"
|
||||||
round
|
round
|
||||||
color="#0c5045"
|
color="#0c5045"
|
||||||
@click="router.push('/assessment')"
|
@click="router.replace('/assessment')"
|
||||||
>
|
>
|
||||||
立即进行健康评估
|
立即进行健康评估
|
||||||
</VanButton>
|
</VanButton>
|
||||||
@@ -213,7 +213,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<!-- 快捷功能入口与操作菜单 -->
|
<!-- 快捷功能入口与操作菜单 -->
|
||||||
<section class="action-card-menu">
|
<section class="action-card-menu">
|
||||||
<div class="menu-item" @click="router.push('/assessment')">
|
<div class="menu-item" @click="router.replace('/assessment')">
|
||||||
<div class="menu-left">
|
<div class="menu-left">
|
||||||
<div class="menu-icon icon-assess">
|
<div class="menu-icon icon-assess">
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
|||||||
Reference in New Issue
Block a user