Skip to content

Navigation Menu

Sign in
Sign up

空调卡片 #12

klhd2023 started this conversation in Show and tell
Discussion options

Home Assistant 空调控制卡片:一级卡片 + 全屏弹出控制面板

分享一个 Home Assistant 空调控制卡片方案:平时显示为紧凑的一级卡片,点击后弹出全屏控制面板,可以控制开关、模式、温度、风速、防直吹、睡眠模式和 ECO 模式。

说明:下面代码里的实体 ID 已经脱敏处理,不能直接照抄使用。使用前请先把示例实体替换成你自己 Home Assistant 里的实体 ID。


效果预览

一级卡片

一级卡片

弹出控制面板

展开查看:弹出控制面板长图 弹出卡片

功能说明

  • 一级卡片显示房间、空调模式、当前温度和设定温度
  • 点击一级卡片后打开全屏控制面板
  • 支持空调开关、制冷、制热、除湿、送风
  • 支持温度加减
  • 支持风速下拉选择
  • 支持防直吹、睡眠模式、ECO 节能模式开关
  • 根据空调模式自动切换主题色

实体替换说明

请把下面这些示例实体替换成你自己的实体:

climate.study_room_ac: 空调主体
switch.study_room_ac_sleep_mode: 睡眠模式开关
switch.study_room_ac_anti_direct_wind: 防直吹 / 风向相关开关
switch.study_room_ac_eco_mode: ECO 节能模式开关
sensor.study_room_temperature: 室内温度传感器
sensor.study_room_humidity: 室内湿度传感器

如果你没有某些功能实体,例如睡眠模式、ECO 或防直吹,可以删除对应实体和底部按钮相关代码,或者改成你自己设备支持的开关。


代码分享

展开查看:完整代码
type: custom:html-pro-card
do_not_parse: true
ignore_line_breaks: true
update_interval: 0
entities:
 - climate.study_room_ac
 - switch.study_room_ac_sleep_mode
 - switch.study_room_ac_anti_direct_wind
 - switch.study_room_ac_eco_mode
 - sensor.study_room_temperature
 - sensor.study_room_humidity
content: >
 <style>
 .ac-mini-card,
 .ac-mini-card * {
 box-sizing: border-box;
 }

 .ac-mini-card {
 --accent: var(--blue-color, #007aff);
 --accent-soft: color-mix(in srgb, var(--accent) 9%, transparent);
 --line: color-mix(in srgb, var(--primary-text-color) 9%, transparent);

 width: 100%;
 min-height: 78px;
 padding: 0;
 border-radius: 10px;
 display: grid;
 grid-template-columns: 0.9fr 0.95fr 1.18fr;
 align-items: stretch;
 color: var(--primary-text-color);
 font: inherit;
 background:
 radial-gradient(circle at 72% 22%, var(--accent-soft), transparent 38%),
 color-mix(in srgb, var(--primary-text-color) 2%, transparent);
 border: 1px solid color-mix(in srgb, var(--primary-text-color) 6%, var(--divider-color));
 box-shadow: 0 10px 28px color-mix(in srgb, var(--primary-text-color) 4%, transparent);
 overflow: hidden;
 cursor: pointer;
 -webkit-tap-highlight-color: transparent;
 transition: transform 0.16s ease, opacity 0.2s ease;
 }

 .ac-mini-card:active {
 transform: scale(0.985);
 }

 .ac-mini-card[data-mode="off"] {
 --accent: var(--grey-color, #8e8e93);
 }

 .ac-mini-card[data-mode="cool"] {
 --accent: var(--blue-color, #007aff);
 }

 .ac-mini-card[data-mode="heat"] {
 --accent: var(--orange-color, #ff9500);
 }

 .ac-mini-card[data-mode="dry"] {
 --accent: var(--green-color, #34c759);
 }

 .ac-mini-card[data-mode="fan_only"] {
 --accent: var(--cyan-color, #32ade6);
 }

 .ac-mini-card[data-mode="auto"],
 .ac-mini-card[data-mode="heat_cool"] {
 --accent: var(--purple-color, #af52de);
 }

 .ac-mini-card[data-mode="unavailable"],
 .ac-mini-card[data-mode="unknown"] {
 opacity: 0.62;
 }

 .segment {
 position: relative;
 min-width: 0;
 min-height: 78px;
 padding: 15px 18px;
 display: grid;
 align-content: center;
 }

 .segment + .segment::before {
 content: "";
 position: absolute;
 left: 0;
 top: 18px;
 bottom: 18px;
 width: 1px;
 background: var(--line);
 border-radius: 10px;
 }

 .room-segment {
 gap: 7px;
 justify-items: start;
 padding-left: 20px;
 padding-right: 14px;
 }

 .mini-label {
 font-size: 10px;
 line-height: 1;
 font-weight: 760;
 letter-spacing: 0.1em;
 color: var(--secondary-text-color);
 text-transform: uppercase;
 white-space: nowrap;
 }

 .room-name {
 font-size: 22px;
 line-height: 1;
 font-weight: 840;
 letter-spacing: -0.065em;
 color: var(--primary-text-color);
 white-space: nowrap;
 overflow: hidden;
 text-overflow: ellipsis;
 }

 .mode-segment {
 justify-items: center;
 gap: 10px;
 text-align: center;
 padding-left: 16px;
 padding-right: 16px;
 }

 .mode-top {
 display: flex;
 align-items: center;
 justify-content: center;
 gap: 7px;
 color: var(--accent);
 }

 .mode-top ha-icon {
 --mdc-icon-size: 18px;
 flex: 0 0 auto;
 }

 .mode-text {
 font-size: 12px;
 line-height: 1;
 font-weight: 850;
 letter-spacing: 0.08em;
 color: var(--accent);
 white-space: nowrap;
 }

 .mode-bar {
 width: 52px;
 height: 4px;
 border-radius: 10px;
 background: color-mix(in srgb, var(--primary-text-color) 9%, transparent);
 overflow: hidden;
 }

 .mode-bar-fill {
 width: 30px;
 height: 100%;
 border-radius: inherit;
 background: var(--accent);
 box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 24%, transparent);
 }

 .ac-mini-card[data-mode="off"] .mode-bar-fill,
 .ac-mini-card[data-mode="unavailable"] .mode-bar-fill,
 .ac-mini-card[data-mode="unknown"] .mode-bar-fill {
 width: 16px;
 }

 .temp-segment {
 grid-template-columns: repeat(2, minmax(0, 1fr));
 gap: 14px;
 align-items: center;
 align-content: center;
 padding-left: 18px;
 padding-right: 18px;
 }

 .temp-item {
 min-width: 0;
 display: grid;
 gap: 7px;
 justify-items: start;
 }

 .temp-label {
 font-size: 10px;
 line-height: 1;
 font-weight: 760;
 letter-spacing: 0.1em;
 color: var(--secondary-text-color);
 white-space: nowrap;
 }

 .temp-value {
 display: flex;
 align-items: flex-start;
 gap: 2px;
 color: var(--primary-text-color);
 font-variant-numeric: tabular-nums;
 }

 .temp-number {
 font-size: 22px;
 line-height: 0.9;
 font-weight: 850;
 letter-spacing: -0.07em;
 white-space: nowrap;
 }

 .temp-unit {
 margin-top: 2px;
 font-size: 10px;
 line-height: 1;
 font-weight: 760;
 color: var(--secondary-text-color);
 white-space: nowrap;
 }

 .temp-item.set .temp-label,
 .temp-item.set .temp-value,
 .temp-item.set .temp-unit {
 color: var(--accent);
 }

 .temp-item.set .temp-unit {
 opacity: 0.82;
 }

 @media (max-width: 430px) {
 .ac-mini-card {
 grid-template-columns: 0.82fr 0.92fr 1.2fr;
 }

 .segment {
 min-height: 76px;
 padding: 14px 12px;
 }

 .room-segment {
 padding-left: 16px;
 padding-right: 10px;
 }

 .room-name {
 font-size: 21px;
 }

 .mode-text {
 font-size: 11px;
 letter-spacing: 0.05em;
 }

 .mode-bar {
 width: 46px;
 }

 .mode-bar-fill {
 width: 27px;
 }

 .temp-segment {
 gap: 10px;
 padding-left: 14px;
 padding-right: 14px;
 }

 .temp-number {
 font-size: 20px;
 }

 .mini-label,
 .temp-label {
 font-size: 9px;
 }
 }
 </style>

 <div class="ac-mini-card" data-role="mini-card" data-mode="unknown"
 aria-label="打开空调控制面板">
 <section class="segment room-segment">
 <div class="mini-label">ROOM</div>
 <div class="room-name" data-role="mini-room">房间</div>
 </section>

 <section class="segment mode-segment">
 <div class="mode-top">
 <ha-icon data-role="mini-mode-icon" icon="mdi:air-conditioner"></ha-icon>
 <div class="mode-text" data-role="mini-mode-text">读取中</div>
 </div>
 <div class="mode-bar">
 <div class="mode-bar-fill"></div>
 </div>
 </section>

 <section class="segment temp-segment">
 <div class="temp-item now">
 <div class="temp-label">当前</div>
 <div class="temp-value">
 <span class="temp-number" data-role="mini-now-temp">--</span>
 <span class="temp-unit">°C</span>
 </div>
 </div>

 <div class="temp-item set">
 <div class="temp-label">设定</div>
 <div class="temp-value">
 <span class="temp-number" data-role="mini-set-temp">--</span>
 <span class="temp-unit">°C</span>
 </div>
 </div>
 </section>
 </div>

 <script>
 (() => {
 if (root.__studyRoomAcMiniFullScreenPanelInit) return;
 root.__studyRoomAcMiniFullScreenPanelInit = true;

 const ENTITY = 'climate.study_room_ac';
 const SLEEP = 'switch.study_room_ac_sleep_mode';
 const WIND = 'switch.study_room_ac_anti_direct_wind';
 const ECO = 'switch.study_room_ac_eco_mode';
 const ROOM_TEMP = 'sensor.study_room_temperature';
 const HUMIDITY = 'sensor.study_room_humidity';

 const $ = (selector) => root.querySelector(selector);
 const stateOf = (id) => hass.states[id];

 const modeMap = {
 off: { text: '已关闭', icon: 'mdi:power' },
 cool: { text: '制冷中', icon: 'mdi:snowflake' },
 heat: { text: '制热中', icon: 'mdi:fire' },
 dry: { text: '除湿中', icon: 'mdi:water-percent' },
 fan_only: { text: '送风中', icon: 'mdi:fan' },
 auto: { text: '自动', icon: 'mdi:autorenew' },
 heat_cool: { text: '自动', icon: 'mdi:autorenew' },
 unavailable: { text: '离线', icon: 'mdi:alert-circle-outline' },
 unknown: { text: '未知', icon: 'mdi:help-circle-outline' }
 };

 const panelModeText = {
 off: '关闭模式',
 cool: '制冷模式',
 heat: '制热模式',
 dry: '除湿模式',
 fan_only: '送风模式',
 unavailable: '设备不可用',
 unknown: '状态未知'
 };

 const fanLabels = {
 auto: '自动',
 silent: '静音',
 quiet: '静音',
 low: '低速',
 medium: '中速',
 middle: '中速',
 high: '强力',
 turbo: '强力',
 strong: '强力'
 };

 const unavailable = (state) => {
 return !state || state.state === 'unknown' || state.state === 'unavailable';
 };

 const clamp = (value, min, max) => Math.max(min, Math.min(max, value));

 const formatNumber = (value) => {
 const number = Number(value);
 if (!Number.isFinite(number)) return '--';
 return Number.isInteger(number) ? String(number) : number.toFixed(1);
 };

 const formatTemp = (value, unit = false) => {
 const text = formatNumber(value);
 if (text === '--') return '--';
 return unit ? `${text}°C` : text;
 };

 const formatSensor = (id, fallbackUnit = '') => {
 const state = stateOf(id);
 if (unavailable(state)) return '--';

 const unit = state.attributes?.unit_of_measurement || fallbackUnit;
 const number = Number(state.state);

 if (Number.isFinite(number)) {
 const text = Number.isInteger(number) ? String(number) : number.toFixed(1);
 return `${text}${unit}`;
 }

 return `${state.state}${unit}`;
 };

 const call = (domain, service, data) => {
 hass.callService(domain, service, data);
 };

 const normalizeTemperature = (value, step) => {
 const raw = String(step);
 const decimals = raw.includes('.') ? raw.split('.')[1].length : 0;
 return Number(value.toFixed(decimals));
 };

 const canSetTemperature = (state) => {
 const attrs = state?.attributes || {};
 const hvac = state?.state;
 const target = Number(attrs.temperature);
 return ['cool', 'heat'].includes(hvac) && Number.isFinite(target);
 };

 const setHvacMode = (mode) => {
 if (!mode) return;

 call('climate', 'set_hvac_mode', {
 entity_id: ENTITY,
 hvac_mode: mode
 });
 };

 const togglePower = () => {
 const state = stateOf(ENTITY);
 if (!state) return;

 const modes = Array.isArray(state.attributes?.hvac_modes)
 ? state.attributes.hvac_modes
 : [];

 if (state.state !== 'off') {
 setHvacMode('off');
 return;
 }

 if (modes.includes('cool')) {
 setHvacMode('cool');
 return;
 }

 setHvacMode(modes.find((mode) => mode !== 'off') || 'cool');
 };

 const adjustTemperature = (direction) => {
 const state = stateOf(ENTITY);
 if (!canSetTemperature(state)) return;

 const attrs = state.attributes || {};
 const step = Number(attrs.target_temp_step ?? 1) || 1;
 const min = Number(attrs.min_temp ?? 16);
 const max = Number(attrs.max_temp ?? 30);
 const current = Number(attrs.temperature);

 const next = normalizeTemperature(
 clamp(current + direction * step, min, max),
 step
 );

 call('climate', 'set_temperature', {
 entity_id: ENTITY,
 temperature: next,
 hvac_mode: state.state
 });
 };

 const labelFan = (value) => {
 const raw = String(value || '');
 if (!raw) return '--';
 const key = raw.toLowerCase();
 return fanLabels[key] || raw.replaceAll('_', ' ');
 };

 const rebuildFanOptions = (select, modes, currentFan) => {
 const safeModes = Array.isArray(modes) ? modes.slice() : [];

 if (currentFan && !safeModes.includes(currentFan)) {
 safeModes.unshift(currentFan);
 }

 const key = safeModes.join('|');

 if (select.dataset.options === key) return;

 select.dataset.options = key;
 select.replaceChildren();

 if (!safeModes.length) {
 const option = document.createElement('option');
 option.value = '';
 option.textContent = '无可用档位';
 option.disabled = true;
 option.selected = true;
 select.appendChild(option);
 return;
 }

 safeModes.forEach((mode) => {
 const option = document.createElement('option');
 option.value = mode;
 option.textContent = labelFan(mode);
 select.appendChild(option);
 });
 };

 const renderToggle = (scope, entity, selector, textSelector, onText, offText) => {
 const button = scope.querySelector(selector);
 const text = scope.querySelector(textSelector);
 const state = stateOf(entity);
 const value = state?.state || 'unknown';

 if (!button) return;

 button.dataset.state = value;
 button.disabled = value === 'unknown' || value === 'unavailable';

 if (text) {
 text.textContent = value === 'on' ? onText : offText;
 }
 };

 const renderMini = () => {
 const state = stateOf(ENTITY);
 const card = $('[data-role="mini-card"]');
 const modeIcon = $('[data-role="mini-mode-icon"]');
 const modeText = $('[data-role="mini-mode-text"]');
 const nowTemp = $('[data-role="mini-now-temp"]');
 const setTemp = $('[data-role="mini-set-temp"]');
 const room = $('[data-role="mini-room"]');

 if (!card || !modeIcon || !modeText || !nowTemp || !setTemp) return;

 const mode = state?.state || 'unknown';
 const attrs = state?.attributes || {};
 const info = modeMap[mode] || modeMap.unknown;

 card.dataset.mode = mode;
 modeIcon.setAttribute('icon', info.icon);
 modeText.textContent = info.text;

 if (room) {
 room.textContent = attrs.friendly_name
 ? attrs.friendly_name.replace('空调', '').trim() || attrs.friendly_name
 : '房间';
 }

 nowTemp.textContent = formatNumber(stateOf(ROOM_TEMP)?.state ?? attrs.current_temperature);
 setTemp.textContent = formatNumber(attrs.temperature);
 };

 const renderPanel = (wrapper) => {
 if (!wrapper || !wrapper.isConnected) return;

 const q = (selector) => wrapper.querySelector(selector);
 const qa = (selector) => Array.from(wrapper.querySelectorAll(selector));

 const state = stateOf(ENTITY);
 const attrs = state?.attributes || {};
 const hvac = state?.state || 'unknown';
 const card = q('[data-role="panel-card"]');

 if (!card) return;

 const modes = Array.isArray(attrs.hvac_modes) ? attrs.hvac_modes : [];
 const fanModes = Array.isArray(attrs.fan_modes) ? attrs.fan_modes : [];
 const currentFan = attrs.fan_mode || '';
 const active = hvac !== 'off' && hvac !== 'unknown' && hvac !== 'unavailable';

 card.dataset.hvac = hvac;

 q('[data-role="panel-title"]').textContent = attrs.friendly_name || '空调';
 q('[data-role="panel-state-text"]').textContent = `${modeMap[hvac]?.text || hvac} · ${hvac === 'unavailable' ? '离线' : '在线'}`;
 q('[data-role="panel-target-temp"]').textContent = formatTemp(attrs.temperature);
 q('[data-role="panel-mode-text"]').textContent = panelModeText[hvac] || hvac || '状态未知';
 q('[data-role="panel-room-temp-text"]').textContent = `室内温度 ${formatSensor(ROOM_TEMP, '°C')}`;
 q('[data-role="panel-humidity-text"]').textContent = `室内湿度 ${formatSensor(HUMIDITY, '%')}`;

 const power = q('[data-action="power"]');

 if (power) {
 power.dataset.on = active ? 'true' : 'false';
 power.disabled = hvac === 'unknown' || hvac === 'unavailable';
 }

 qa('[data-action="temp-down"], [data-action="temp-up"]').forEach((button) => {
 button.disabled = !canSetTemperature(state);
 });

 qa('[data-action="mode"]').forEach((button) => {
 const mode = button.dataset.mode;
 const supported = !modes.length || modes.includes(mode);

 button.classList.toggle('is-active', hvac === mode);
 button.disabled = !supported || hvac === 'unknown' || hvac === 'unavailable';
 });

 const fanSelect = q('[data-role="panel-fan-select"]');

 if (fanSelect) {
 rebuildFanOptions(fanSelect, fanModes, currentFan);

 if (currentFan) {
 fanSelect.value = currentFan;
 }

 fanSelect.disabled = !fanModes.length || hvac === 'unknown' || hvac === 'unavailable';
 }

 renderToggle(wrapper, WIND, '[data-entity="' + WIND + '"]', '[data-role="panel-wind-text"]', '已开启', '防直吹');
 renderToggle(wrapper, SLEEP, '[data-entity="' + SLEEP + '"]', '[data-role="panel-sleep-text"]', '已开启', '未开启');
 renderToggle(wrapper, ECO, '[data-entity="' + ECO + '"]', '[data-role="panel-eco-text"]', '已开启', '未开启');
 };

 const closeOverlay = () => {
 const wrapper = root.__studyRoomAcFullScreenOverlay;
 if (!wrapper) return;

 wrapper.classList.add('is-closing');

 window.setTimeout(() => {
 wrapper.remove();
 root.__studyRoomAcFullScreenOverlay = null;
 }, 180);
 };

 const openOverlay = () => {
 if (root.__studyRoomAcFullScreenOverlay && root.__studyRoomAcFullScreenOverlay.isConnected) {
 root.__studyRoomAcFullScreenOverlay.remove();
 root.__studyRoomAcFullScreenOverlay = null;
 }

 const wrapper = document.createElement('div');
 wrapper.className = 'ac-control-overlay';

 wrapper.innerHTML = `
 <style>
 .ac-control-overlay,
 .ac-control-overlay * {
 box-sizing: border-box;
 }

 .ac-control-overlay {
 position: fixed;
 inset: 0;
 z-index: 99999;
 pointer-events: auto;
 color: var(--primary-text-color);
 font: inherit;
 opacity: 1;
 background: #edf6ff;
 transition: opacity 0.18s ease;
 }

 .ac-control-overlay.is-closing {
 opacity: 0;
 }

 .ac-full-page {
 position: absolute;
 inset: 0;
 width: 100vw;
 height: 100vh;
 height: 100dvh;
 overflow: auto;
 background: #edf6ff;
 }

 .ac-panel,
 .ac-panel * {
 box-sizing: border-box;
 }

 .ac-panel {
 --mode-color: #2f7df6;
 --mode-soft: rgba(47, 125, 246, 0.12);
 --mode-mid: rgba(47, 125, 246, 0.22);
 --text-main: #17243a;
 --text-sub: #7d8fa8;
 --card-shadow: 0 14px 32px rgba(48, 83, 132, 0.15);

 width: 100%;
 min-height: 100vh;
 min-height: 100dvh;
 padding: calc(22px + env(safe-area-inset-top)) 22px calc(24px + env(safe-area-inset-bottom));
 margin: 0;
 border-radius: 10px;
 overflow: hidden;
 color: var(--text-main);
 font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", sans-serif;
 background:
 radial-gradient(circle at 50% 21%, var(--mode-soft), transparent 35%),
 linear-gradient(180deg, #f7fbff 0%, #edf6ff 100%);
 }

 .ac-panel[data-hvac="cool"] {
 --mode-color: #2f7df6;
 --mode-soft: rgba(47, 125, 246, 0.13);
 --mode-mid: rgba(47, 125, 246, 0.23);
 }

 .ac-panel[data-hvac="heat"] {
 --mode-color: #ff8a2a;
 --mode-soft: rgba(255, 138, 42, 0.14);
 --mode-mid: rgba(255, 138, 42, 0.24);
 }

 .ac-panel[data-hvac="dry"] {
 --mode-color: #20b86a;
 --mode-soft: rgba(32, 184, 106, 0.13);
 --mode-mid: rgba(32, 184, 106, 0.22);
 }

 .ac-panel[data-hvac="fan_only"] {
 --mode-color: #20a8d8;
 --mode-soft: rgba(32, 168, 216, 0.13);
 --mode-mid: rgba(32, 168, 216, 0.23);
 }

 .ac-panel[data-hvac="off"],
 .ac-panel[data-hvac="unknown"],
 .ac-panel[data-hvac="unavailable"] {
 --mode-color: #94a3b8;
 --mode-soft: rgba(148, 163, 184, 0.13);
 --mode-mid: rgba(148, 163, 184, 0.22);
 }

 .ac-header {
 display: grid;
 grid-template-columns: minmax(0, 1fr) auto;
 gap: 12px;
 align-items: start;
 }

 .title-block {
 min-width: 0;
 display: grid;
 gap: 8px;
 }

 .room-title {
 font-size: 20px;
 line-height: 1.05;
 font-weight: 800;
 letter-spacing: -0.04em;
 color: #263751;
 white-space: nowrap;
 overflow: hidden;
 text-overflow: ellipsis;
 }

 .status-row {
 display: flex;
 align-items: center;
 gap: 7px;
 min-width: 0;
 color: var(--text-sub);
 font-size: 12px;
 font-weight: 700;
 }

 .status-dot {
 width: 7px;
 height: 7px;
 border-radius: 10px;
 background: var(--mode-color);
 box-shadow: 0 0 0 4px var(--mode-soft);
 flex: 0 0 auto;
 }

 .header-actions {
 display: flex;
 align-items: center;
 gap: 8px;
 }

 .title-icon-btn {
 width: 40px;
 height: 40px;
 border: 0;
 border-radius: 10px;
 display: grid;
 place-items: center;
 background: rgba(255, 255, 255, 0.78);
 box-shadow: 0 8px 18px rgba(58, 96, 145, 0.12);
 cursor: pointer;
 padding: 0;
 -webkit-tap-highlight-color: transparent;
 transition: transform 0.15s ease, background 0.2s ease, color 0.2s ease, opacity 0.2s ease;
 }

 .title-icon-btn:active {
 transform: scale(0.94);
 }

 .title-icon-btn:disabled {
 opacity: 0.45;
 cursor: default;
 }

 .title-icon-btn ha-icon {
 --mdc-icon-size: 21px;
 }

 .power-btn {
 color: var(--mode-color);
 }

 .power-btn[data-on="true"] {
 color: #ffffff;
 background: var(--mode-color);
 box-shadow: 0 10px 22px var(--mode-mid);
 }

 .close-btn {
 color: #7d8fa8;
 }

 .hero {
 padding: clamp(74px, 13vh, 112px) 0 clamp(44px, 8vh, 62px);
 text-align: center;
 }

 .target {
 display: inline-flex;
 align-items: flex-start;
 color: var(--mode-color);
 text-shadow: 0 18px 36px var(--mode-soft);
 font-variant-numeric: tabular-nums;
 }

 .target-num {
 font-size: clamp(90px, 28vw, 126px);
 line-height: 0.82;
 font-weight: 620;
 letter-spacing: -0.085em;
 }

 .target-unit {
 margin-top: 7px;
 margin-left: 5px;
 font-size: 38px;
 line-height: 1;
 font-weight: 680;
 }

 .mode-label {
 margin-top: 24px;
 color: var(--mode-color);
 font-size: 14px;
 font-weight: 760;
 }

 .sensor-text-row {
 margin-top: 10px;
 display: flex;
 justify-content: center;
 align-items: center;
 gap: 10px;
 color: var(--text-sub);
 font-size: 14px;
 line-height: 1;
 font-weight: 700;
 font-variant-numeric: tabular-nums;
 }

 .sensor-dot {
 width: 4px;
 height: 4px;
 border-radius: 10px;
 background: color-mix(in srgb, var(--text-sub) 55%, transparent);
 }

 .temp-actions {
 margin: 0 auto clamp(38px, 6vh, 54px);
 display: flex;
 justify-content: center;
 gap: 44px;
 }

 .round-btn {
 width: 64px;
 height: 64px;
 border: 0;
 border-radius: 10px;
 display: grid;
 place-items: center;
 color: var(--mode-color);
 background: linear-gradient(145deg, rgba(255,255,255,0.96), rgba(229,239,252,0.94));
 box-shadow:
 0 14px 28px rgba(63, 102, 158, 0.15),
 inset 0 1px 1px rgba(255,255,255,0.92);
 cursor: pointer;
 padding: 0;
 -webkit-tap-highlight-color: transparent;
 transition: transform 0.15s ease, opacity 0.15s ease;
 }

 .round-btn:active {
 transform: scale(0.94);
 }

 .round-btn:disabled {
 opacity: 0.38;
 cursor: default;
 }

 .round-btn ha-icon {
 --mdc-icon-size: 26px;
 }

 .section + .section {
 margin-top: 24px;
 }

 .section-title {
 margin: 0 0 14px 6px;
 color: #5b6f8a;
 font-size: 13px;
 line-height: 1;
 font-weight: 800;
 }

 .mode-pill {
 height: 72px;
 padding: 7px;
 border-radius: 10px;
 display: grid;
 grid-template-columns: repeat(4, minmax(0, 1fr));
 gap: 4px;
 background: rgba(226, 237, 251, 0.86);
 box-shadow:
 inset 0 2px 5px rgba(99, 128, 171, 0.08),
 0 12px 26px rgba(69, 103, 153, 0.10);
 }

 .mode-btn {
 --btn-color: #2f7df6;

 border: 0;
 border-radius: 10px;
 background: transparent;
 color: #73859e;
 display: grid;
 justify-items: center;
 align-content: center;
 gap: 6px;
 cursor: pointer;
 font: inherit;
 font-size: 11px;
 font-weight: 780;
 padding: 0;
 -webkit-tap-highlight-color: transparent;
 transition: transform 0.15s ease, background 0.18s ease, color 0.18s ease;
 }

 .mode-btn[data-mode="cool"] {
 --btn-color: #2f7df6;
 }

 .mode-btn[data-mode="heat"] {
 --btn-color: #ff8a2a;
 }

 .mode-btn[data-mode="dry"] {
 --btn-color: #20b86a;
 }

 .mode-btn[data-mode="fan_only"] {
 --btn-color: #20a8d8;
 }

 .mode-btn ha-icon {
 --mdc-icon-size: 21px;
 color: var(--btn-color);
 opacity: 0.72;
 }

 .mode-btn.is-active {
 color: var(--btn-color);
 background: rgba(255, 255, 255, 0.96);
 box-shadow: 0 8px 20px rgba(57, 93, 145, 0.12);
 }

 .mode-btn.is-active ha-icon {
 opacity: 1;
 }

 .mode-btn:active {
 transform: scale(0.96);
 }

 .mode-btn:disabled {
 opacity: 0.35;
 cursor: default;
 }

 .fan-row {
 height: 60px;
 border-radius: 10px;
 padding: 0 14px;
 display: grid;
 grid-template-columns: 38px minmax(0, 1fr) auto;
 gap: 12px;
 align-items: center;
 background: rgba(255,255,255,0.74);
 box-shadow: var(--card-shadow);
 }

 .fan-icon {
 width: 38px;
 height: 38px;
 border-radius: 10px;
 display: grid;
 place-items: center;
 color: #20a8d8;
 background: rgba(32, 168, 216, 0.12);
 }

 .fan-icon ha-icon {
 --mdc-icon-size: 20px;
 }

 .fan-copy {
 min-width: 0;
 display: grid;
 gap: 5px;
 }

 .fan-main {
 color: #334964;
 font-size: 14px;
 line-height: 1;
 font-weight: 820;
 }

 .fan-sub {
 color: var(--text-sub);
 font-size: 11px;
 line-height: 1;
 font-weight: 650;
 }

 .select-box {
 position: relative;
 display: grid;
 place-items: center;
 }

 .fan-select {
 width: 126px;
 height: 38px;
 border: 0;
 outline: none;
 border-radius: 10px;
 padding: 0 32px 0 12px;
 color: var(--mode-color);
 background: linear-gradient(145deg, rgba(255,255,255,0.96), rgba(235,243,253,0.94));
 box-shadow: inset 0 0 0 1px var(--mode-soft);
 font: inherit;
 font-size: 13px;
 font-weight: 850;
 appearance: none;
 -webkit-appearance: none;
 cursor: pointer;
 text-align: center;
 }

 .select-box::after {
 content: "";
 position: absolute;
 right: 12px;
 top: 50%;
 width: 7px;
 height: 7px;
 border-right: 2px solid var(--mode-color);
 border-bottom: 2px solid var(--mode-color);
 transform: translateY(-65%) rotate(45deg);
 pointer-events: none;
 opacity: 0.75;
 }

 .fan-select:disabled {
 opacity: 0.45;
 cursor: default;
 }

 .bottom-grid {
 display: grid;
 grid-template-columns: repeat(3, minmax(0, 1fr));
 gap: 14px;
 margin-top: 18px;
 }

 .mini-card {
 min-height: 110px;
 padding: 16px 12px 14px;
 border: 0;
 border-radius: 10px;
 background: linear-gradient(145deg, rgba(255,255,255,0.88), rgba(229,239,251,0.92));
 box-shadow: var(--card-shadow);
 color: #5f718b;
 text-align: left;
 font: inherit;
 cursor: pointer;
 -webkit-tap-highlight-color: transparent;
 display: grid;
 align-content: space-between;
 transition: transform 0.15s ease, opacity 0.15s ease;
 }

 .mini-card:active {
 transform: scale(0.97);
 }

 .mini-card:disabled {
 opacity: 0.45;
 cursor: default;
 }

 .mini-icon {
 width: 34px;
 height: 34px;
 border-radius: 10px;
 display: grid;
 place-items: center;
 background: rgba(47, 125, 246, 0.11);
 color: #2f7df6;
 }

 .mini-card[data-kind="sleep"] .mini-icon {
 background: rgba(124, 109, 240, 0.12);
 color: #7c6df0;
 }

 .mini-card[data-kind="eco"] .mini-icon {
 background: rgba(37, 196, 107, 0.13);
 color: #25c46b;
 }

 .mini-card[data-state="on"] {
 background: linear-gradient(145deg, rgba(255,255,255,0.96), rgba(219,236,255,0.96));
 box-shadow:
 0 16px 32px var(--mode-soft),
 inset 0 0 0 1px var(--mode-soft);
 }

 .mini-icon ha-icon {
 --mdc-icon-size: 20px;
 }

 .mini-label {
 margin-top: 12px;
 color: #7c8da4;
 font-size: 12px;
 font-weight: 750;
 }

 .mini-value {
 margin-top: 4px;
 color: #12203a;
 font-size: 15px;
 line-height: 1.05;
 font-weight: 900;
 word-break: keep-all;
 }

 @media (max-width: 390px) {
 .ac-panel {
 padding-left: 18px;
 padding-right: 18px;
 }

 .header-actions {
 gap: 7px;
 }

 .title-icon-btn {
 width: 38px;
 height: 38px;
 }

 .hero {
 padding-top: 78px;
 padding-bottom: 46px;
 }

 .target-num {
 font-size: clamp(84px, 29vw, 116px);
 }

 .fan-select {
 width: 112px;
 }

 .bottom-grid {
 gap: 10px;
 }
 }
 </style>

 <div class="ac-full-page">
 <div class="ac-panel" data-role="panel-card" data-hvac="unknown">
 <header class="ac-header">
 <div class="title-block">
 <div class="room-title" data-role="panel-title">空调</div>
 <div class="status-row">
 <span class="status-dot"></span>
 <span data-role="panel-state-text">读取中 · 在线</span>
 </div>
 </div>

 <div class="header-actions">
 <button class="title-icon-btn power-btn" type="button" data-action="power" data-on="false" aria-label="切换空调电源">
 <ha-icon icon="mdi:power"></ha-icon>
 </button>

 <button class="title-icon-btn close-btn" type="button" data-action="close" aria-label="关闭空调控制面板">
 <ha-icon icon="mdi:close"></ha-icon>
 </button>
 </div>
 </header>

 <section class="hero">
 <div class="target">
 <span class="target-num" data-role="panel-target-temp">--</span>
 <span class="target-unit">°</span>
 </div>

 <div class="mode-label" data-role="panel-mode-text">读取中</div>

 <div class="sensor-text-row">
 <span data-role="panel-room-temp-text">室内温度 --</span>
 <span class="sensor-dot"></span>
 <span data-role="panel-humidity-text">室内湿度 --</span>
 </div>
 </section>

 <div class="temp-actions">
 <button class="round-btn" type="button" data-action="temp-down" aria-label="降低温度">
 <ha-icon icon="mdi:minus"></ha-icon>
 </button>

 <button class="round-btn" type="button" data-action="temp-up" aria-label="升高温度">
 <ha-icon icon="mdi:plus"></ha-icon>
 </button>
 </div>

 <section class="section">
 <div class="section-title">模式选择</div>
 <div class="mode-pill">
 <button class="mode-btn" type="button" data-action="mode" data-mode="cool">
 <ha-icon icon="mdi:snowflake"></ha-icon>
 <span>制冷</span>
 </button>

 <button class="mode-btn" type="button" data-action="mode" data-mode="heat">
 <ha-icon icon="mdi:white-balance-sunny"></ha-icon>
 <span>制热</span>
 </button>

 <button class="mode-btn" type="button" data-action="mode" data-mode="dry">
 <ha-icon icon="mdi:water-outline"></ha-icon>
 <span>除湿</span>
 </button>

 <button class="mode-btn" type="button" data-action="mode" data-mode="fan_only">
 <ha-icon icon="mdi:weather-windy"></ha-icon>
 <span>送风</span>
 </button>
 </div>
 </section>

 <section class="section">
 <div class="section-title">风速调节</div>
 <div class="fan-row">
 <div class="fan-icon">
 <ha-icon icon="mdi:fan"></ha-icon>
 </div>

 <div class="fan-copy">
 <div class="fan-main">风速</div>
 <div class="fan-sub">下拉选择档位</div>
 </div>

 <div class="select-box">
 <select class="fan-select" data-role="panel-fan-select" aria-label="选择风速"></select>
 </div>
 </div>
 </section>

 <section class="bottom-grid">
 <button class="mini-card" type="button" data-action="toggle" data-kind="wind" data-entity="${WIND}">
 <div class="mini-icon">
 <ha-icon icon="mdi:swap-vertical"></ha-icon>
 </div>
 <div>
 <div class="mini-label">风向</div>
 <div class="mini-value" data-role="panel-wind-text">防直吹</div>
 </div>
 </button>

 <button class="mini-card" type="button" data-action="toggle" data-kind="sleep" data-entity="${SLEEP}">
 <div class="mini-icon">
 <ha-icon icon="mdi:bed-outline"></ha-icon>
 </div>
 <div>
 <div class="mini-label">睡眠</div>
 <div class="mini-value" data-role="panel-sleep-text">未开启</div>
 </div>
 </button>

 <button class="mini-card" type="button" data-action="toggle" data-kind="eco" data-entity="${ECO}">
 <div class="mini-icon">
 <ha-icon icon="mdi:leaf"></ha-icon>
 </div>
 <div>
 <div class="mini-label">节能</div>
 <div class="mini-value" data-role="panel-eco-text">未开启</div>
 </div>
 </button>
 </section>
 </div>
 </div>
 `;

 const host = typeof overlay !== 'undefined' && overlay ? overlay : document.body;
 host.appendChild(wrapper);
 root.__studyRoomAcFullScreenOverlay = wrapper;

 wrapper.addEventListener('click', (event) => {
 const target = event.target.closest('[data-action]');
 if (!target || !wrapper.contains(target) || target.disabled) return;

 const action = target.dataset.action;

 if (action === 'close') {
 closeOverlay();
 return;
 }

 if (action === 'power') {
 togglePower();
 }

 if (action === 'temp-down') {
 adjustTemperature(-1);
 }

 if (action === 'temp-up') {
 adjustTemperature(1);
 }

 if (action === 'mode') {
 setHvacMode(target.dataset.mode);
 }

 if (action === 'toggle') {
 const entityId = target.dataset.entity;
 if (!entityId) return;

 call(entityId.split('.')[0], 'toggle', {
 entity_id: entityId
 });
 }
 });

 const fanSelect = wrapper.querySelector('[data-role="panel-fan-select"]');

 if (fanSelect) {
 fanSelect.addEventListener('change', () => {
 if (!fanSelect.value) return;

 call('climate', 'set_fan_mode', {
 entity_id: ENTITY,
 fan_mode: fanSelect.value
 });
 });
 }

 renderPanel(wrapper);
 };

 $('[data-role="mini-card"]').addEventListener('click', openOverlay);

 root._watchedEntities = [ENTITY, SLEEP, WIND, ECO, ROOM_TEMP, HUMIDITY];

 root._onHassUpdate = () => {
 renderMini();

 if (root.__studyRoomAcFullScreenOverlay && root.__studyRoomAcFullScreenOverlay.isConnected) {
 renderPanel(root.__studyRoomAcFullScreenOverlay);
 }
 };

 renderMini();
 })();
 </script>
You must be logged in to vote

Replies: 0 comments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
1 participant

AltStyle によって変換されたページ (->オリジナル) /