11
This commit is contained in:
+24
-1
@@ -75,6 +75,7 @@ export type HomeAssistantDiscoveredDevice = {
|
||||
areaName?: string
|
||||
category: HealthDeviceCategory | string
|
||||
online: boolean
|
||||
primaryControlEntityId?: string | null
|
||||
entities: HomeAssistantEntity[]
|
||||
metrics?: Record<string, unknown>
|
||||
}
|
||||
@@ -102,6 +103,25 @@ export type HomeAssistantEnvironmentData = {
|
||||
tvoc?: number | null
|
||||
}
|
||||
|
||||
export type HomeAssistantScenePreset = {
|
||||
deviceName: string
|
||||
category: string
|
||||
entityId: string
|
||||
available: boolean
|
||||
availabilityMessage: string
|
||||
action: string
|
||||
detail: string
|
||||
parameters: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type HomeAssistantDeviceSwitchOutput = {
|
||||
success: boolean
|
||||
deviceId: string
|
||||
entityId: string
|
||||
state: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export type HomeAssistantStatus = {
|
||||
connected: boolean
|
||||
message: string
|
||||
@@ -110,6 +130,7 @@ export type HomeAssistantStatus = {
|
||||
devices: HomeAssistantDiscoveredDevice[]
|
||||
sceneDevices: HomeAssistantSceneDevicesSummary
|
||||
environment: HomeAssistantEnvironmentData
|
||||
scenePresets?: HomeAssistantScenePreset[]
|
||||
}
|
||||
|
||||
export type HomeAssistantSceneAction = {
|
||||
@@ -145,5 +166,7 @@ export const healthApi = {
|
||||
executeSleepScene: () =>
|
||||
http.post<never, HomeAssistantSceneResult>('/api/health/home-assistant/scenes/sleep'),
|
||||
executeAllOffScene: () =>
|
||||
http.post<never, HomeAssistantSceneResult>('/api/health/home-assistant/scenes/all-off')
|
||||
http.post<never, HomeAssistantSceneResult>('/api/health/home-assistant/scenes/all-off'),
|
||||
switchDevice: (payload: { deviceId: string; turnOn: boolean; entityId?: string }) =>
|
||||
http.post<never, HomeAssistantDeviceSwitchOutput>('/api/health/home-assistant/devices/switch', payload)
|
||||
}
|
||||
|
||||
+199
-9
@@ -245,6 +245,23 @@ button { cursor: pointer; }
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
.preset-toggle-btn {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--green);
|
||||
background: #edf7f3;
|
||||
padding: 3px 9px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(12, 80, 69, 0.15);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.preset-toggle-btn:active {
|
||||
background: var(--green);
|
||||
color: #fff;
|
||||
}
|
||||
.scene-status-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
@@ -267,6 +284,27 @@ button { cursor: pointer; }
|
||||
.device-mini-status.yellow { background: #fef7ec; color: #b8860b; }
|
||||
.device-mini-status.gray { background: #f1f3f2; color: #82908c; }
|
||||
|
||||
.sensor-mini-sync-btn {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 3px 9px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(18, 63, 55, 0.15);
|
||||
background: #fff;
|
||||
color: var(--green);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.sensor-mini-sync-btn:active {
|
||||
background: #edf7f3;
|
||||
}
|
||||
.sensor-mini-sync-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.scene-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -327,7 +365,7 @@ button { cursor: pointer; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 10px;
|
||||
padding: 6px 9px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(18, 63, 55, 0.06);
|
||||
border-radius: 10px;
|
||||
@@ -354,16 +392,112 @@ button { cursor: pointer; }
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.scene-device-chip .desc {
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
.chip-toggle-btn {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 7px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(12, 80, 69, 0.2);
|
||||
background: #f1f3f2;
|
||||
color: #73817d;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.chip-toggle-btn.on {
|
||||
background: #0c5045;
|
||||
color: #ffffff;
|
||||
border-color: #0c5045;
|
||||
}
|
||||
.chip-toggle-btn.disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
background: #f1f3f2;
|
||||
color: #9aa4a1;
|
||||
border-color: #e2e5e3;
|
||||
}
|
||||
|
||||
/* 场景联动执行参数详情面板 */
|
||||
.scene-presets-panel {
|
||||
margin-top: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed rgba(18, 63, 55, 0.12);
|
||||
}
|
||||
.preset-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.preset-panel-header span {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--deep);
|
||||
}
|
||||
.preset-panel-header small {
|
||||
font-size: 10px;
|
||||
color: #82908c;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.preset-items-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.preset-card-item {
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(18, 63, 55, 0.06);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
box-shadow: 0 1px 2px rgba(18, 63, 55, 0.02);
|
||||
}
|
||||
.preset-card-item.unavailable {
|
||||
background: #f7f8f7;
|
||||
opacity: 0.72;
|
||||
}
|
||||
.preset-card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.preset-icon {
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.preset-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--deep);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.preset-action {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
padding: 1px 6px;
|
||||
border-radius: 6px;
|
||||
background: #edf7f3;
|
||||
color: var(--green);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.preset-detail {
|
||||
margin: 3px 0 0 20px;
|
||||
font-size: 11px;
|
||||
color: #657470;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.preset-availability {
|
||||
margin: 4px 0 0 20px;
|
||||
font-size: 10px;
|
||||
color: var(--green);
|
||||
}
|
||||
.preset-availability.unavailable {
|
||||
color: #9a6a38;
|
||||
}
|
||||
.scene-device-chip.green .desc { color: var(--green); font-weight: 500; }
|
||||
.scene-device-chip.yellow .desc { color: #b8860b; font-weight: 500; }
|
||||
.scene-device-chip.gray .desc { color: #9aa4a1; }
|
||||
|
||||
/* Environment Health */
|
||||
.env-container {
|
||||
@@ -554,10 +688,66 @@ button { cursor: pointer; }
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
gap: 3px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.device-switch-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px 2px 5px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(18, 63, 55, 0.15);
|
||||
background: #f1f3f2;
|
||||
color: #6b7774;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.device-switch-pill .switch-knob {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: #9aa4a1;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.device-switch-pill.on {
|
||||
background: #edf7f3;
|
||||
border-color: rgba(16, 185, 129, 0.4);
|
||||
color: var(--green);
|
||||
}
|
||||
.device-switch-pill.on .switch-knob {
|
||||
background: #10b981;
|
||||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
.device-switch-pill.disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.sensor-sync-pill {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(18, 63, 55, 0.15);
|
||||
background: #fff;
|
||||
color: var(--green);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.sensor-sync-pill:active {
|
||||
background: #edf7f3;
|
||||
}
|
||||
.sensor-sync-pill:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
|
||||
+158
-31
@@ -44,17 +44,15 @@ const totalDeviceCount = computed(() => homeAssistant.value?.summary?.totalDevic
|
||||
const onlineDeviceCount = computed(() => homeAssistant.value?.summary?.onlineDevices ?? homeAssistant.value?.devices?.filter(d => d.online).length ?? 0)
|
||||
const totalEntityCount = computed(() => homeAssistant.value?.summary?.totalEntities ?? 0)
|
||||
const availableEntityCount = computed(() => homeAssistant.value?.summary?.availableEntities ?? 0)
|
||||
const sceneTotal = computed(() => homeAssistant.value?.sceneDevices?.total ?? 4)
|
||||
const sceneTotal = computed(() => homeAssistant.value?.sceneDevices?.total ?? 0)
|
||||
const sceneAvailable = computed(() => homeAssistant.value?.sceneDevices?.available ?? 0)
|
||||
|
||||
const sceneDevices = computed(() => {
|
||||
if (!homeAssistant.value?.devices) return []
|
||||
return homeAssistant.value.devices.filter(d =>
|
||||
d.category === 'EnvironmentControl' ||
|
||||
d.category === 'Fragrance' ||
|
||||
d.category === 'Lighting' ||
|
||||
d.category === 'Media'
|
||||
const configuredCategories = new Set(
|
||||
(homeAssistant.value.scenePresets || []).map(item => item.category)
|
||||
)
|
||||
return homeAssistant.value.devices.filter(d => configuredCategories.has(d.category))
|
||||
})
|
||||
|
||||
const envDevice = computed(() => {
|
||||
@@ -151,6 +149,67 @@ function getHumidityTag(hum: number | undefined | null): string {
|
||||
return '潮湿'
|
||||
}
|
||||
|
||||
const switchingDeviceId = ref<string | null>(null)
|
||||
const syncingSensor = ref(false)
|
||||
const showScenePresets = ref(true)
|
||||
|
||||
function isControllable(device: HomeAssistantDiscoveredDevice): boolean {
|
||||
return !!device.primaryControlEntityId
|
||||
}
|
||||
|
||||
function getPrimaryControlEntity(device: HomeAssistantDiscoveredDevice) {
|
||||
if (!device.primaryControlEntityId) return undefined
|
||||
return device.entities.find(e => e.entityId === device.primaryControlEntityId)
|
||||
}
|
||||
|
||||
function isPrimaryControlAvailable(device: HomeAssistantDiscoveredDevice): boolean {
|
||||
return !!getPrimaryControlEntity(device)?.available
|
||||
}
|
||||
|
||||
function isDeviceTurnedOn(device: HomeAssistantDiscoveredDevice): boolean {
|
||||
const target = getPrimaryControlEntity(device)
|
||||
if (!target) return false
|
||||
return target.state === 'on' || target.state === 'playing'
|
||||
}
|
||||
|
||||
async function toggleDeviceSwitch(device: HomeAssistantDiscoveredDevice) {
|
||||
if (!isPrimaryControlAvailable(device)) {
|
||||
showToast('设备主控功能当前不可用')
|
||||
return
|
||||
}
|
||||
const currentOn = isDeviceTurnedOn(device)
|
||||
const nextOn = !currentOn
|
||||
switchingDeviceId.value = device.deviceId
|
||||
try {
|
||||
const res = await healthApi.switchDevice({
|
||||
deviceId: device.deviceId,
|
||||
turnOn: nextOn,
|
||||
entityId: device.primaryControlEntityId || undefined
|
||||
})
|
||||
showToast({
|
||||
type: res.success ? 'success' : 'fail',
|
||||
message: res.message || (nextOn ? '已开启设备' : '已关闭设备')
|
||||
})
|
||||
homeAssistant.value = await healthApi.homeAssistantStatus()
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : '控制失败')
|
||||
} finally {
|
||||
switchingDeviceId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function syncSensorData() {
|
||||
syncingSensor.value = true
|
||||
try {
|
||||
homeAssistant.value = await healthApi.homeAssistantStatus()
|
||||
showToast({ type: 'success', message: '状态已刷新' })
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : '刷新失败')
|
||||
} finally {
|
||||
syncingSensor.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -282,20 +341,27 @@ onMounted(load)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. 场景控制板块 -->
|
||||
<!-- 1. 睡眠场景控制 -->
|
||||
<div class="smart-subcard">
|
||||
<div class="subcard-header">
|
||||
<div class="subcard-title">
|
||||
<span class="subcard-icon">🌙</span>
|
||||
<strong>睡眠场景控制</strong>
|
||||
<span class="subcard-model">({{ sceneAvailable }}/{{ sceneTotal }} 项可用)</span>
|
||||
</div>
|
||||
<span class="scene-status-badge">场景 {{ sceneAvailable }}/{{ sceneTotal }} 就绪</span>
|
||||
<button
|
||||
class="preset-toggle-btn"
|
||||
type="button"
|
||||
@click="showScenePresets = !showScenePresets"
|
||||
>
|
||||
{{ showScenePresets ? '收起参数 ▲' : '控制参数详情 ▼' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="scene-row">
|
||||
<div class="scene-info">
|
||||
<strong>舒睡模式</strong>
|
||||
<p>除湿 55% · 微风睡眠 · 暖光助眠 · 草本香氛</p>
|
||||
<strong>一键舒睡</strong>
|
||||
<p>仅执行已配置且当前可用的恒湿、香氛与暖光动作</p>
|
||||
</div>
|
||||
<div class="scene-actions">
|
||||
<button
|
||||
@@ -311,12 +377,12 @@ onMounted(load)
|
||||
:disabled="!homeAssistant?.connected || sceneAvailable === 0 || !!sceneRunning"
|
||||
@click="executeScene('all-off')"
|
||||
>
|
||||
{{ sceneRunning === 'all-off' ? '执行中…' : '全部关闭' }}
|
||||
{{ sceneRunning === 'all-off' ? '执行中…' : '关闭场景设备' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 场景关联的4个设备微卡片 -->
|
||||
<!-- 场景设备实时可用状态 -->
|
||||
<div v-if="sceneDevices.length" class="scene-device-list">
|
||||
<div
|
||||
v-for="d in sceneDevices"
|
||||
@@ -326,7 +392,37 @@ onMounted(load)
|
||||
>
|
||||
<span class="dot"></span>
|
||||
<span class="name">{{ getSceneDeviceShortName(d) }}</span>
|
||||
<span class="desc">{{ getSceneDeviceStatus(d) }}</span>
|
||||
<span class="device-mini-status" :class="getDeviceStatusType(d)">
|
||||
{{ getSceneDeviceStatus(d) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 后台真实配置的场景动作 -->
|
||||
<div v-if="showScenePresets && homeAssistant?.scenePresets?.length" class="scene-presets-panel">
|
||||
<div class="preset-panel-header">
|
||||
<span>场景执行参数</span>
|
||||
<small>仅展示后台实际下发的动作</small>
|
||||
</div>
|
||||
<div class="preset-items-grid">
|
||||
<div
|
||||
v-for="item in homeAssistant.scenePresets"
|
||||
:key="item.deviceName"
|
||||
class="preset-card-item"
|
||||
:class="{ unavailable: !item.available }"
|
||||
>
|
||||
<div class="preset-card-top">
|
||||
<span class="preset-icon">
|
||||
{{ item.category === 'EnvironmentControl' ? '💧' : item.category === 'Fragrance' ? '🌸' : item.category === 'Lighting' ? '💡' : item.category === 'Media' ? '🔊' : item.category === 'Sleep' ? '🛏️' : item.category === 'EnvironmentSensor' ? '🌿' : '⚖️' }}
|
||||
</span>
|
||||
<strong class="preset-name">{{ item.deviceName }}</strong>
|
||||
<span class="preset-action">{{ item.action }}</span>
|
||||
</div>
|
||||
<p class="preset-detail">{{ item.detail }}</p>
|
||||
<p class="preset-availability" :class="{ unavailable: !item.available }">
|
||||
{{ item.availabilityMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -339,9 +435,14 @@ onMounted(load)
|
||||
<strong>环境健康</strong>
|
||||
<span class="subcard-model">{{ getDeviceSubModel(envDevice, '罗威亚检测仪') }}</span>
|
||||
</div>
|
||||
<span v-if="envDevice" class="device-mini-status" :class="getDeviceStatusType(envDevice)">
|
||||
{{ getDeviceShortBadgeText(envDevice) }}
|
||||
</span>
|
||||
<button
|
||||
class="sensor-mini-sync-btn"
|
||||
:disabled="syncingSensor"
|
||||
type="button"
|
||||
@click="syncSensorData"
|
||||
>
|
||||
{{ syncingSensor ? '刷新中…' : '立即刷新' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="env-container">
|
||||
@@ -408,7 +509,11 @@ onMounted(load)
|
||||
<strong>睡眠健康</strong>
|
||||
<span class="subcard-model">{{ getDeviceSubModel(sleepDevice, '8H 睡眠床垫') }}</span>
|
||||
</div>
|
||||
<span v-if="sleepDevice" class="device-mini-status" :class="getDeviceStatusType(sleepDevice)">
|
||||
<span
|
||||
v-if="sleepDevice"
|
||||
class="device-mini-status"
|
||||
:class="getDeviceStatusType(sleepDevice)"
|
||||
>
|
||||
{{ getDeviceShortBadgeText(sleepDevice) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -419,15 +524,15 @@ onMounted(load)
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<small>左侧软硬度</small>
|
||||
<strong>{{ sleepDevice?.metrics?.leftFirmness || '5 档 · 适中' }}</strong>
|
||||
<strong>{{ sleepDevice?.metrics?.leftFirmness || '暂无数据' }}</strong>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<small>右侧软硬度</small>
|
||||
<strong>{{ sleepDevice?.metrics?.rightFirmness || '6 档 · 偏硬' }}</strong>
|
||||
<strong>{{ sleepDevice?.metrics?.rightFirmness || '暂无数据' }}</strong>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<small>智能调温</small>
|
||||
<strong>{{ sleepDevice?.online ? '温控已就绪' : '待接入' }}</strong>
|
||||
<strong>{{ sleepDevice?.online ? '状态待读取' : '待接入' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -440,22 +545,27 @@ onMounted(load)
|
||||
<strong>身体健康</strong>
|
||||
<span class="subcard-model">{{ getDeviceSubModel(bodyDevice, '米家体脂秤') }}</span>
|
||||
</div>
|
||||
<span v-if="bodyDevice" class="device-mini-status" :class="getDeviceStatusType(bodyDevice)">
|
||||
{{ getDeviceShortBadgeText(bodyDevice) }}
|
||||
</span>
|
||||
<button
|
||||
class="sensor-mini-sync-btn"
|
||||
:disabled="syncingSensor"
|
||||
type="button"
|
||||
@click="syncSensorData"
|
||||
>
|
||||
{{ syncingSensor ? '刷新中…' : '刷新状态' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="device-feature-row">
|
||||
<div class="feature-item">
|
||||
<small>体脂秤状态</small>
|
||||
<strong>{{ bodyDevice?.online ? '称重即同步' : '待同步' }}</strong>
|
||||
<strong>{{ bodyDevice?.online ? '设备在线' : '待同步' }}</strong>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<small>最新体重</small>
|
||||
<strong>{{ bodyDevice?.metrics?.weight || '68.5' }}<em>kg</em></strong>
|
||||
<strong>{{ bodyDevice?.metrics?.weight ?? '—' }}<em v-if="bodyDevice?.metrics?.weight != null">kg</em></strong>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<small>体脂率</small>
|
||||
<strong>{{ bodyDevice?.metrics?.bodyFatRate || '18.2' }}<em>%</em></strong>
|
||||
<strong>{{ bodyDevice?.metrics?.bodyFatRate ?? '—' }}<em v-if="bodyDevice?.metrics?.bodyFatRate != null">%</em></strong>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<small>测量算法</small>
|
||||
@@ -472,7 +582,7 @@ onMounted(load)
|
||||
<strong>全部智能设备</strong>
|
||||
<span class="subcard-model">({{ totalDeviceCount }} 台)</span>
|
||||
</div>
|
||||
<small style="color: #82908c; font-size: 11px;">自动发现实时同步</small>
|
||||
<small style="color: #82908c; font-size: 11px;">已确认主控能力的设备可操作</small>
|
||||
</div>
|
||||
|
||||
<div v-if="homeAssistant?.devices?.length" class="all-device-list">
|
||||
@@ -490,15 +600,32 @@ onMounted(load)
|
||||
<span class="category-tag">{{ categoryNames[item.category] || item.category }}</span>
|
||||
</div>
|
||||
<p class="device-card-meta">
|
||||
<span>{{ item.areaName || '睡眠空间' }}</span>
|
||||
<span>{{ item.areaName || '未分区' }}</span>
|
||||
<span>· {{ item.model }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-card-extra">
|
||||
<span class="status-pill" :class="getDeviceStatusType(item)">
|
||||
{{ getDeviceListBadgeText(item) }}
|
||||
</span>
|
||||
<button
|
||||
v-if="isControllable(item)"
|
||||
class="device-switch-pill"
|
||||
:class="{ on: isDeviceTurnedOn(item), disabled: !isPrimaryControlAvailable(item) }"
|
||||
:disabled="!isPrimaryControlAvailable(item) || switchingDeviceId === item.deviceId"
|
||||
type="button"
|
||||
@click="toggleDeviceSwitch(item)"
|
||||
>
|
||||
<span class="switch-knob"></span>
|
||||
<span class="switch-text">{{ switchingDeviceId === item.deviceId ? '…' : (isDeviceTurnedOn(item) ? '已开启' : '已关闭') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="sensor-sync-pill"
|
||||
:disabled="syncingSensor"
|
||||
type="button"
|
||||
@click="syncSensorData"
|
||||
>
|
||||
{{ syncingSensor ? '同步中' : '刷新' }}
|
||||
</button>
|
||||
<small class="entity-count">{{ item.entities.filter(e => e.available).length }}/{{ item.entities.length }} 实体</small>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
Reference in New Issue
Block a user