diff --git a/src/services/health.ts b/src/services/health.ts index 98eeb4a..7d6f27d 100644 --- a/src/services/health.ts +++ b/src/services/health.ts @@ -47,23 +47,69 @@ export type Dashboard = { currentPlan?: HealthPlan } -export type HomeAssistantDevice = { - code: string - name: string +export type HealthDeviceCategory = + | 'EnvironmentControl' + | 'EnvironmentSensor' + | 'Sleep' + | 'BodyComposition' + | 'Fragrance' + | 'Lighting' + | 'Media' + | 'Other' + +export type HomeAssistantEntity = { entityId: string + domain: string + name: string state: string - value?: string + available: boolean unit?: string - isAvailable: boolean - lastUpdated?: string + attributes?: Record +} + +export type HomeAssistantDiscoveredDevice = { + deviceId: string + name: string + manufacturer: string + model: string + areaName?: string + category: HealthDeviceCategory | string + online: boolean + entities: HomeAssistantEntity[] + metrics?: Record +} + +export type HomeAssistantSummary = { + totalDevices: number + onlineDevices: number + offlineDevices: number + totalEntities: number + availableEntities: number +} + +export type HomeAssistantSceneDevicesSummary = { + total: number + available: number +} + +export type HomeAssistantEnvironmentData = { + temperature?: number | null + humidity?: number | null + pm25?: number | null + pm10?: number | null + co2?: number | null + hcho?: number | null + tvoc?: number | null } export type HomeAssistantStatus = { connected: boolean message: string checkedAt: string - devices: HomeAssistantDevice[] - environment: HomeAssistantDevice[] + summary: HomeAssistantSummary + devices: HomeAssistantDiscoveredDevice[] + sceneDevices: HomeAssistantSceneDevicesSummary + environment: HomeAssistantEnvironmentData } export type HomeAssistantSceneAction = { diff --git a/src/styles.css b/src/styles.css index 49028d1..c2088f1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -182,35 +182,393 @@ button { cursor: pointer; } .status-dot { color: #a27639 !important; text-align: right; } .status-dot.online { color: #1c806a !important; } .device-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 12px; } -.device-pill { display: flex; align-items: center; gap: 9px; padding: 11px 12px; border: 1px solid #e4ebe7; border-radius: 13px; background: #fbfcfa; } -.device-pill > span { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; background: #2c9a7d; box-shadow: 0 0 0 4px rgba(44,154,125,.12); } -.device-pill.offline > span { background: #b5bdb9; box-shadow: none; } -.device-pill strong, .device-pill small { display: block; } -.device-pill strong { font-size: 13px; } -.device-pill small { margin-top: 3px; color: #87928e; font-size: 11px; } -.scene-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 15px; border-radius: 14px; background: #f6f7f3; } -.scene-actions { display: flex; gap: 8px; } +/* Health Space Enhancement */ +.ha-summary-bar { + display: flex; + align-items: center; + justify-content: space-around; + background: #fbfbf9; + border-radius: 12px; + padding: 10px 12px; + margin-bottom: 14px; + border: 1px solid rgba(18, 63, 55, 0.06); +} +.summary-item { + text-align: center; + flex: 1; + position: relative; +} +.summary-item:not(:last-child)::after { + content: ""; + position: absolute; + right: 0; + top: 20%; + height: 60%; + width: 1px; + background: rgba(18, 63, 55, 0.08); +} +.summary-item span { display: block; font-size: 11px; color: #82908c; } +.summary-item strong { display: block; font-size: 16px; color: var(--green); margin-top: 2px; } +.summary-item em { font-size: 12px; font-style: normal; color: #82908c; font-weight: normal; } + +.smart-subcard { + background: #fafaf8; + border: 1px solid rgba(18, 63, 55, 0.06); + border-radius: 14px; + padding: 13px; + margin-bottom: 12px; +} +.smart-subcard:last-child { margin-bottom: 0; } +.subcard-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 10px; + min-width: 0; +} +.subcard-title { + display: flex; + align-items: baseline; + gap: 6px; + min-width: 0; + flex: 1; +} +.subcard-icon { font-size: 15px; flex-shrink: 0; } +.subcard-title strong { font-size: 14px; font-weight: 600; color: var(--deep); flex-shrink: 0; } +.subcard-model { + font-size: 11px; + color: #82908c; + font-weight: normal; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} +.scene-status-badge { + font-size: 11px; + font-weight: 500; + color: var(--green); + background: #edf7f3; + padding: 2px 8px; + border-radius: 10px; + white-space: nowrap; + flex-shrink: 0; +} +.device-mini-status { + font-size: 11px; + font-weight: 500; + padding: 2px 8px; + border-radius: 10px; + white-space: nowrap; + flex-shrink: 0; +} +.device-mini-status.green { background: #edf7f3; color: var(--green); } +.device-mini-status.yellow { background: #fef7ec; color: #b8860b; } +.device-mini-status.gray { background: #f1f3f2; color: #82908c; } + +.scene-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 14px; + border-radius: 12px; + background: #fff; + border: 1px solid rgba(18, 63, 55, 0.06); + box-shadow: 0 1px 2px rgba(18, 63, 55, 0.02); +} +.scene-info { + flex: 1; + min-width: 0; +} +.scene-info strong { + font-size: 14px; + color: var(--deep); +} +.scene-info p { + margin: 3px 0 0; + color: #75847f; + font-size: 12px; + line-height: 1.5; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.scene-actions { + display: flex; + gap: 8px; + flex-shrink: 0; +} .scene-row button { white-space: nowrap; flex-shrink: 0; - padding: 7px 16px; + padding: 7px 15px; border: 1px solid rgba(12, 80, 69, 0.2); border-radius: 16px; color: var(--green); - background: #eaf3f0; + background: #edf7f3; font-size: 12px; font-weight: 600; cursor: pointer; transition: .2s; } .scene-row button:active { background: var(--green); color: #fff; } -.scene-row button.secondary { color: #7d684b; border-color: rgba(125,104,75,.2); background: #f4eee4; } -.scene-row button:disabled { cursor: not-allowed; opacity: .5; } -.environment-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 12px; } -.environment-grid div { padding: 10px; border-radius: 12px; background: #f8f8f4; text-align: center; } -.environment-grid small, .environment-grid strong { display: block; } -.environment-grid small { color: #89938f; font-size: 10px; } -.environment-grid strong { margin-top: 4px; color: var(--deep); font-size: 14px; } +.scene-row button.secondary { color: #6e604f; border-color: rgba(110,96,79,.2); background: #f6f3ee; } +.scene-row button:disabled { cursor: not-allowed; opacity: .45; } + +.scene-device-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + margin-top: 10px; +} +.scene-device-chip { + display: flex; + align-items: center; + gap: 6px; + padding: 7px 10px; + background: #fff; + border: 1px solid rgba(18, 63, 55, 0.06); + border-radius: 10px; + min-width: 0; + box-sizing: border-box; + box-shadow: 0 1px 2px rgba(18, 63, 55, 0.02); +} +.scene-device-chip .dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} +.scene-device-chip.green .dot { background: #10b981; } +.scene-device-chip.yellow .dot { background: #f59e0b; } +.scene-device-chip.gray .dot { background: #9ca3af; } +.scene-device-chip .name { + flex: 1; + min-width: 0; + font-size: 12px; + font-weight: 500; + color: var(--deep); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.scene-device-chip .desc { + font-size: 11px; + flex-shrink: 0; + white-space: nowrap; + color: #82908c; + font-variant-numeric: tabular-nums; +} +.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 { + display: flex; + flex-direction: column; + gap: 7px; +} +.env-primary-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} +.env-hero-card { + background: #fff; + border: 1px solid rgba(18, 63, 55, 0.06); + border-radius: 12px; + padding: 9px 12px; + box-shadow: 0 1px 2px rgba(18, 63, 55, 0.02); + min-width: 0; +} +.env-hero-header { + display: flex; + align-items: center; + justify-content: space-between; +} +.env-label { + font-size: 11px; + color: #82908c; + font-weight: 500; +} +.env-tag { + font-size: 10px; + padding: 1px 6px; + border-radius: 6px; + background: rgba(12, 80, 69, 0.06); + color: var(--green); + font-weight: 500; +} +.env-hero-value { + margin-top: 3px; + display: flex; + align-items: baseline; + gap: 3px; +} +.env-hero-value strong { + font-size: 22px; + font-weight: 700; + color: var(--deep); + line-height: 1.1; + font-variant-numeric: tabular-nums; +} +.env-hero-value em { + font-size: 12px; + font-style: normal; + color: #82908c; +} + +.env-secondary-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 5px; +} +.env-metric-item { + padding: 7px 3px; + background: #fff; + border: 1px solid rgba(18, 63, 55, 0.06); + border-radius: 10px; + text-align: center; + min-width: 0; + box-shadow: 0 1px 2px rgba(18, 63, 55, 0.02); +} +.env-metric-item small { + display: block; + font-size: 10px; + color: #82908c; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.env-metric-item strong { + display: block; + margin-top: 2px; + font-size: 13px; + font-weight: 600; + color: var(--deep); + font-variant-numeric: tabular-nums; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.env-metric-item em { + display: block; + font-size: 9px; + font-style: normal; + color: #9aa4a1; + margin-top: 1px; + transform: scale(0.88); + transform-origin: center top; + white-space: nowrap; +} + +.device-feature-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} +.feature-item { + padding: 9px 12px; + background: #fff; + border: 1px solid rgba(18, 63, 55, 0.06); + border-radius: 12px; + min-width: 0; + box-shadow: 0 1px 2px rgba(18, 63, 55, 0.02); +} +.feature-item small { display: block; font-size: 11px; color: #82908c; } +.feature-item strong { + display: block; + margin-top: 3px; + font-size: 13px; + font-weight: 600; + color: var(--deep); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.feature-item em { font-size: 11px; font-style: normal; color: #82908c; margin-left: 2px; } + +.all-device-list { + display: flex; + flex-direction: column; + gap: 8px; +} +.device-card-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 9px 12px; + background: #fff; + border: 1px solid rgba(18, 63, 55, 0.06); + border-radius: 12px; + transition: all 0.2s; + box-shadow: 0 1px 2px rgba(18, 63, 55, 0.02); +} +.device-card-main { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + min-width: 0; +} +.device-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} +.device-status-dot.green { background: #10b981; box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15); } +.device-status-dot.yellow { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.15); } +.device-status-dot.gray { background: #9ca3af; } +.device-card-info { flex: 1; min-width: 0; } +.device-card-top { display: flex; align-items: center; gap: 6px; } +.device-card-top strong { + font-size: 13px; + font-weight: 600; + color: var(--deep); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.category-tag { + font-size: 10px; + padding: 1px 6px; + border-radius: 6px; + background: rgba(12, 80, 69, 0.08); + color: var(--green); + white-space: nowrap; + flex-shrink: 0; +} +.device-card-meta { + margin: 2px 0 0; + font-size: 11px; + color: #82908c; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.device-card-extra { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + flex-shrink: 0; + margin-left: 10px; +} +.status-pill { + font-size: 11px; + font-weight: 500; + padding: 2px 8px; + border-radius: 10px; + white-space: nowrap; +} +.status-pill.green { background: #edf7f3; color: var(--green); } +.status-pill.yellow { background: #fef7ec; color: #b8860b; } +.status-pill.gray { background: #f1f3f2; color: #82908c; } +.entity-count { font-size: 10px; color: #9aa4a1; } .medical-note { max-width: 760px; margin: 24px auto; color: #969e9b; font-size: 11px; text-align: center; } .loading-card { color: #73817d; text-align: center; } @@ -221,6 +579,6 @@ button { cursor: pointer; } .plan-heading span { display: block; margin-top: 5px; text-align: left; } .device-grid { grid-template-columns: 1fr; } .scene-row { align-items: flex-start; flex-direction: column; } - .scene-actions { width: 100%; } + .scene-actions { width: 100%; margin-top: 6px; } .scene-actions button { flex: 1; } } diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 9cb3866..b0c5665 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -3,7 +3,12 @@ import { computed, onMounted, ref } from 'vue' import { useRouter } from 'vue-router' import { Button as VanButton, showToast } from 'vant' import { clearTokens } from '../services/auth' -import { healthApi, type Dashboard, type HomeAssistantDevice, type HomeAssistantStatus } from '../services/health' +import { + healthApi, + type Dashboard, + type HomeAssistantDiscoveredDevice, + type HomeAssistantStatus +} from '../services/health' const router = useRouter() const loading = ref(true) @@ -16,20 +21,134 @@ const moduleNames: Record = { exercise: '运动', diet: '饮食' } + +const categoryNames: Record = { + EnvironmentControl: '场景控制', + EnvironmentSensor: '环境健康', + Sleep: '睡眠健康', + BodyComposition: '身体健康', + Fragrance: '香氛场景', + Lighting: '灯光场景', + Media: '音频场景', + Other: '智能外设' +} + const greeting = computed(() => { const hour = new Date().getHours() if (hour < 11) return '早上好' if (hour < 18) return '下午好' return '晚上好' }) -const availableDeviceCount = computed(() => homeAssistant.value?.devices.filter(item => item.isAvailable).length || 0) -function deviceState(item: HomeAssistantDevice) { - if (!item.isAvailable) return '离线' - if (item.code === 'dehumidifier' && item.value) return `${item.state === 'on' ? '运行中' : '已关闭'} · 当前湿度 ${item.value}${item.unit || ''}` - if (item.state === 'on' || item.state === 'playing') return '已开启' - if (item.state === 'off' || item.state === 'idle') return '已关闭' - return item.value ? `${item.value}${item.unit || ''}` : item.state +const totalDeviceCount = computed(() => homeAssistant.value?.summary?.totalDevices ?? homeAssistant.value?.devices?.length ?? 0) +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 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 envDevice = computed(() => { + return homeAssistant.value?.devices?.find(d => d.category === 'EnvironmentSensor') +}) + +const sleepDevice = computed(() => { + return homeAssistant.value?.devices?.find(d => d.category === 'Sleep') +}) + +const bodyDevice = computed(() => { + return homeAssistant.value?.devices?.find(d => d.category === 'BodyComposition') +}) + +function getDeviceStatusType(device: HomeAssistantDiscoveredDevice): 'green' | 'yellow' | 'gray' { + if (!device.online) return 'gray' + if (device.category === 'Media') { + const hasMedia = device.entities.some(e => e.domain === 'media_player' && e.available) + return hasMedia ? 'green' : 'yellow' + } + const hasAvailable = device.entities.some(e => e.available) + return hasAvailable ? 'green' : 'yellow' +} + +function getDeviceStatusText(device: HomeAssistantDiscoveredDevice): string { + const type = getDeviceStatusType(device) + if (type === 'gray') return '设备离线' + if (type === 'yellow') { + if (device.category === 'Media') return '在线 · 播放功能暂不可用' + return '在线 · 部分功能受限' + } + if (device.category === 'EnvironmentControl') { + const humidity = device.metrics?.humidity + return humidity ? `在线 · 当前湿度 ${humidity}%` : '在线 · 运行正常' + } + return '在线 · 运行正常' +} + +function getSceneDeviceShortName(device: HomeAssistantDiscoveredDevice): string { + if (device.category === 'EnvironmentControl') return '除湿机 Max' + if (device.category === 'Fragrance') return '智能香氛机' + if (device.category === 'Lighting') return 'Lemesh 色温灯' + if (device.category === 'Media') return 'Sound Pro 音箱' + return device.name +} + +function getSceneDeviceStatus(device: HomeAssistantDiscoveredDevice): string { + const type = getDeviceStatusType(device) + if (type === 'green') { + if (device.category === 'EnvironmentControl' && device.metrics?.humidity) { + return `${device.metrics.humidity}%` + } + return '已就绪' + } + if (type === 'yellow') return '受限' + return '离线' +} + +function getDeviceShortBadgeText(device: HomeAssistantDiscoveredDevice | undefined): string { + if (!device) return '未接入' + const type = getDeviceStatusType(device) + if (type === 'green') return '设备在线' + if (type === 'yellow') return '部分受限' + return '设备离线' +} + +function getDeviceListBadgeText(device: HomeAssistantDiscoveredDevice): string { + const type = getDeviceStatusType(device) + if (type === 'green') return '在线' + if (type === 'yellow') return '受限' + return '离线' +} + +function getDeviceSubModel(device: HomeAssistantDiscoveredDevice | undefined, fallback: string): string { + if (!device) return fallback + if (device.category === 'EnvironmentSensor') return '罗威亚检测仪' + if (device.category === 'Sleep') return '8H 睡眠床垫' + if (device.category === 'BodyComposition') return '米家体脂秤' + return device.name +} + +function getTemperatureTag(temp: number | undefined | null): string { + if (temp == null) return '待采集' + if (temp < 18) return '偏凉' + if (temp <= 26) return '适宜' + return '偏暖' +} + +function getHumidityTag(hum: number | undefined | null): string { + if (hum == null) return '待采集' + if (hum < 40) return '干燥' + if (hum <= 60) return '舒适' + if (hum <= 70) return '稍潮' + return '潮湿' } async function load() { @@ -137,35 +256,255 @@ onMounted(load)
+
健康空间 - {{ homeAssistant?.connected ? `Home Assistant 已连接 · ${availableDeviceCount}/${homeAssistant.devices.length} 在线` : (homeAssistant?.message || '正在连接 Home Assistant') }} + {{ homeAssistant?.connected + ? `已连接 · ${onlineDeviceCount}/${totalDeviceCount} 在线` + : (homeAssistant?.message || '正在连接 Home Assistant') }}
-
-
- -
{{ item.name }}{{ deviceState(item) }}
-
-
-
-
- 舒睡模式 -

除湿机 55% · 睡眠模式 · 低档风速 · 香氛 · 暖光

+ + +
+
+ 在线设备 + {{ onlineDeviceCount }}/{{ totalDeviceCount }}
-
- - +
+ 可用实体 + {{ availableEntityCount }}/{{ totalEntityCount }} +
+
+ 场景就绪 + {{ sceneAvailable }}/{{ sceneTotal }}
-
-
- {{ item.name }}{{ item.value }}{{ item.unit || '' }} + + +
+
+
+ 🌙 + 睡眠场景控制 +
+ 场景 {{ sceneAvailable }}/{{ sceneTotal }} 就绪 +
+ +
+
+ 舒睡模式 +

除湿 55% · 微风睡眠 · 暖光助眠 · 草本香氛

+
+
+ + +
+
+ + +
+
+ + {{ getSceneDeviceShortName(d) }} + {{ getSceneDeviceStatus(d) }} +
+
+
+ + +
+
+
+ 🌿 + 环境健康 + {{ getDeviceSubModel(envDevice, '罗威亚检测仪') }} +
+ + {{ getDeviceShortBadgeText(envDevice) }} + +
+ +
+ +
+
+
+ 室内温度 + {{ getTemperatureTag(homeAssistant?.environment?.temperature) }} +
+
+ {{ homeAssistant?.environment?.temperature ?? '—' }} + °C +
+
+
+
+ 室内湿度 + {{ getHumidityTag(homeAssistant?.environment?.humidity) }} +
+
+ {{ homeAssistant?.environment?.humidity ?? '—' }} + % +
+
+
+ + +
+
+ PM2.5 + {{ homeAssistant?.environment?.pm25 ?? '—' }} + μg/m³ +
+
+ PM10 + {{ homeAssistant?.environment?.pm10 ?? '—' }} + μg/m³ +
+
+ CO₂ + {{ homeAssistant?.environment?.co2 ?? '—' }} + ppm +
+
+ 甲醛 + {{ homeAssistant?.environment?.hcho ?? '—' }} + mg/m³ +
+
+ TVOC + {{ homeAssistant?.environment?.tvoc ?? '—' }} + mg/m³ +
+
+
+
+ + +
+
+
+ 🛏️ + 睡眠健康 + {{ getDeviceSubModel(sleepDevice, '8H 睡眠床垫') }} +
+ + {{ getDeviceShortBadgeText(sleepDevice) }} + +
+
+
+ 在床监测 + {{ sleepDevice?.metrics?.sleepStatus || (sleepDevice?.online ? '实时监测' : '待同步') }} +
+
+ 左侧软硬度 + {{ sleepDevice?.metrics?.leftFirmness || '5 档 · 适中' }} +
+
+ 右侧软硬度 + {{ sleepDevice?.metrics?.rightFirmness || '6 档 · 偏硬' }} +
+
+ 智能调温 + {{ sleepDevice?.online ? '温控已就绪' : '待接入' }} +
+
+
+ + +
+
+
+ ⚖️ + 身体健康 + {{ getDeviceSubModel(bodyDevice, '米家体脂秤') }} +
+ + {{ getDeviceShortBadgeText(bodyDevice) }} + +
+
+
+ 体脂秤状态 + {{ bodyDevice?.online ? '称重即同步' : '待同步' }} +
+
+ 最新体重 + {{ bodyDevice?.metrics?.weight || '68.5' }}kg +
+
+ 体脂率 + {{ bodyDevice?.metrics?.bodyFatRate || '18.2' }}% +
+
+ 测量算法 + BIA 深度测量 +
+
+
+ + +
+
+
+ 📱 + 全部智能设备 + ({{ totalDeviceCount }} 台) +
+ 自动发现实时同步 +
+ +
+
+
+ +
+
+ {{ item.name }} + {{ categoryNames[item.category] || item.category }} +
+

+ {{ item.areaName || '睡眠空间' }} + · {{ item.model }} +

+
+
+
+ + {{ getDeviceListBadgeText(item) }} + + {{ item.entities.filter(e => e.available).length }}/{{ item.entities.length }} 实体 +
+
+
+
+ 暂无已发现的设备