import { LitElement, html, css } from "https://unpkg.com/lit-element@2.4.0/lit-element.js?module"; class XiaoshiTodoButtonEditor extends LitElement { static get properties() { return { hass: { type: Object }, config: { type: Object }, _searchTerm: { type: String }, _filteredEntities: { type: Array }, _showEntityList: { type: Boolean } }; } static get styles() { return css` .form { display: flex; flex-direction: column; gap: 10px; min-height: 500px; } .form-group { display: flex; flex-direction: column; gap: 5px; } label { font-weight: bold; } select, input, textarea { padding: 8px; border: 1px solid #ddd; border-radius: 4px; } textarea { min-height: 80px; resize: vertical; } .help-text { font-size: 0.85em; color: #666; margin-top: 4px; } .entity-selector { position: relative; } .entity-search-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; } .entity-dropdown { position: absolute; top: 100%; left: 0; right: 0; height: 300px; overflow-y: auto; background: white; border: 1px solid #ddd; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); z-index: 1000; margin-top: 2px; } .entity-option { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; cursor: pointer; border-bottom: 1px solid #eee; } .entity-option:hover { background: #f5f5f5; } .entity-option.selected { background: #e3f2fd; } .entity-info { display: flex; align-items: center; gap: 8px; flex: 1; } .entity-details { flex: 1; } .entity-name { font-weight: 500; font-size: 14px; color: #000; } .entity-id { font-size: 12px; color: #000; font-family: monospace; } .check-icon { color: #4CAF50; } .no-results { padding: 12px; text-align: center; color: #666; font-style: italic; } .selected-entities { margin-top: 8px; } .selected-label { font-size: 12px; font-weight: bold; margin-bottom: 4px; color: #333; } .selected-entity { display: inline-flex; align-items: center; gap: 4px; background: #f0f0f0; padding: 4px 8px; border-radius: 16px; margin: 2px 4px 2px 0; font-size: 12px; color: #000; } .remove-btn { background: none; border: none; cursor: pointer; padding: 0; display: flex; align-items: center; color: #f44336; } .remove-btn:hover { color: #d32f2f; } /*button新元素 开始*/ .checkbox-group { display: flex; align-items: center; gap: 0; margin: 0; padding: 0; } .checkbox-input { margin: 0; } .checkbox-label { font-weight: normal; margin: 0; } /*button新元素 结束*/ `; } render() { if (!this.hass) return html``; return html`
${this._showEntityList ? html`
${this._filteredEntities.map(entity => html`
this._toggleEntity(entity.entity_id)} >
${entity.attributes.friendly_name || entity.entity_id}
${entity.entity_id}
${this.config.entities && this.config.entities.includes(entity.entity_id) ? html`` : ''}
`)} ${this._filteredEntities.length === 0 ? html`
未找到匹配的实体
` : ''}
` : ''}
${this.config.entities && this.config.entities.length > 0 ? html`
已选择的实体:
${this.config.entities.map(entityId => { const entity = this.hass.states[entityId]; return html`
${entity?.attributes.friendly_name || entityId}
`; })} ` : ''}
搜索并选择要显示的待办事项实体,支持多选
`; } _entityChanged(e) { /*button新按钮方法 开始*/ const { name, value, type, checked } = e.target; let finalValue; // 处理复选框 if (type === 'checkbox') { finalValue = checked; } else { if (!value && name !== 'theme' && name !== 'button_width' && name !== 'button_height' && name !== 'button_font_size' && name !== 'button_icon_size' && name !== 'width' && name !== 'tap_action') return; finalValue = value } // 处理不同字段的默认值 if (name === 'button_width') { finalValue = value || '100%'; } else if (name === 'button_height') { finalValue = value || '24px'; } else if (name === 'button_font_size') { finalValue = value || '11px'; } else if (name === 'button_icon_size') { finalValue = value || '13px'; } else if (name === 'width') { finalValue = value || '100%'; } else if (name === 'tap_action') { // 处理 tap_action 的特殊逻辑 if (value === 'tap_action') { // 如果是弹出卡片,则不设置 tap_action,让组件使用默认逻辑 finalValue = undefined; } else { finalValue = value; } } /*button新按钮方法 结束*/ this.config = { ...this.config, [name]: finalValue }; this.dispatchEvent(new CustomEvent('config-changed', { detail: { config: this.config }, bubbles: true, composed: true })); } _onEntitySearch(e) { const searchTerm = e.target.value.toLowerCase(); this._searchTerm = searchTerm; this._showEntityList = true; if (!this.hass) return; // 获取所有实体 const allEntities = Object.values(this.hass.states); // 过滤实体,默认只显示todo.开头的实体 this._filteredEntities = allEntities.filter(entity => { const entityId = entity.entity_id.toLowerCase(); const friendlyName = (entity.attributes.friendly_name || '').toLowerCase(); // 默认只显示todo.开头的实体,或者搜索时匹配搜索词 const isTodoEntity = entityId.startsWith('todo.'); const matchesSearch = entityId.includes(searchTerm) || friendlyName.includes(searchTerm); return isTodoEntity && matchesSearch; }).slice(0, 50); // 限制显示数量 this.requestUpdate(); } _toggleEntity(entityId) { const currentEntities = this.config.entities || []; let newEntities; if (currentEntities.includes(entityId)) { // 移除实体 newEntities = currentEntities.filter(id => id !== entityId); } else { // 添加实体 newEntities = [...currentEntities, entityId]; } this.config = { ...this.config, entities: newEntities }; this.dispatchEvent(new CustomEvent('config-changed', { detail: { config: this.config }, bubbles: true, composed: true })); this.requestUpdate(); } _removeEntity(entityId) { const currentEntities = this.config.entities || []; const newEntities = currentEntities.filter(id => id !== entityId); this.config = { ...this.config, entities: newEntities }; this.dispatchEvent(new CustomEvent('config-changed', { detail: { config: this.config }, bubbles: true, composed: true })); this.requestUpdate(); } // 点击外部关闭下拉列表 firstUpdated() { document.addEventListener('click', (e) => { if (!e.target.closest('.entity-selector')) { this._showEntityList = false; this.requestUpdate(); } }); } constructor() { super(); this._searchTerm = ''; this._filteredEntities = []; this._showEntityList = false; } setConfig(config) { this.config = config; } } customElements.define('xiaoshi-todo-button-editor', XiaoshiTodoButtonEditor); class XiaoshiTodoButton extends LitElement { static get properties() { return { hass: Object, config: Object, _todoData: Array, _loading: Boolean, _refreshInterval: Number, _dataLoaded: Boolean, //button新元素 theme: { type: String }, _editingItem: { type: Object }, _expandedAddForm: { type: Object } }; } static get styles() { return css` :host { display: block; width: var(--card-width, 100%); } /*button新元素 开始*/ .todo-status { width: var(--button-width, 65px); height: var(--button-height, 24px); padding: 0; margin: 0; background: var(--bg-color, #fff); color: var(--fg-color, #000); border-radius: 10px; font-size: var(--button-font-size, 11px); font-weight: 500; text-align: center; box-sizing: border-box; display: flex; align-items: center; justify-content: center; gap: 0; cursor: pointer; transition: background-color 0.2s, transform 0.1s; position: relative; } .status-icon { --mdc-icon-size: var(--button-icon-size, 13px); color: var(--fg-color, #000); margin-right: 3px; } /* 角标模式样式 */ .todo-status.badge-mode { width: var(--button-width, 65px); height: var(--button-height, 24px); border-radius: 10px; padding: 0; margin: 0; display: flex; align-items: center; justify-content: center; } .todo-status.badge-mode .status-icon { color: rgb(128, 128, 128); transition: color 0.2s; } .todo-status.badge-mode.has-warning .status-icon { color: rgb(255, 0, 0); } .badge-number { position: absolute; top: -6px; right: -6px; min-width: 12px; height: 12px; background: rgb(255, 0, 0); color: rgb(255, 255, 255); border-radius: 50%; font-size: 8px; font-weight: bold; display: flex; align-items: center; justify-content: center; padding: 0; box-sizing: border-box; line-height: 1; } /*button新元素 结束*/ ha-card { width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--bg-color, #fff); border-radius: 12px; } /*标题容器*/ .card-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; background: var(--bg-color, #fff); border-radius: 12px; } /*标题红色圆点*/ .offline-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; background: rgb(255, 165, 0); } /*标题红色圆点动画*/ @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } } /*标题*/ .card-title { font-size: 20px; font-weight: 500; color: var(--fg-color, #000); height: 30px; line-height: 30px; display: flex; align-items: center; justify-content: center; } /*标题统计数字*/ .device-count { color: var(--fg-color, #000); border-radius: 8px; font-size: 13px; width: 30px; height: 30px; text-align: center; line-height: 30px; font-weight: bold; padding: 0px; background: rgb(255, 165, 0); } /*标题刷新按钮*/ .refresh-btn { color: var(--fg-color, #fff); border: none; border-radius: 8px; padding: 5px; cursor: pointer; font-size: 13px; width: 50px; height: 30px; line-height: 30px; text-align: center; font-weight: bold; padding: 0px; background: rgb(255, 165, 0); } /*2级标题*/ .section-divider { margin: 0 0 8px 0; padding: 8px 8px; background: var(--bg-color, #fff); font-weight: 500; color: var(--fg-color, #000); border-top: 1px solid rgb(150,150,150,0.5); border-bottom: 1px solid rgb(150,150,150,0.5); margin: 0 16px 0 16px; } /*2级标题字体*/ .section-title { display: flex; align-items: center; justify-content: space-between; color: var(--fg-color, #000); font-size: 13px; } /*2级标题,统计数量字体*/ .section-count { background: rgb(255,165,0); color: var(--fg-color, #000); border-radius: 12px; width: 15px; height: 15px; text-align: center; line-height: 15px; padding: 3px; font-size: 12px; font-weight: bold; } /*设备、实体明细*/ .device-item { display: flex; align-items: center; padding: 0px; border-bottom: 1px solid rgb(150,150,150,0.2); margin: 0 32px 0px 32px; } /*设备、实体明细背景*/ .devices-list { flex: 1; overflow-y: auto; min-height: 0; padding: 0 0 8px 0; } .device-icon { margin-right: 12px; color: var(--error-color); } .device-info { flex-grow: 1; padding: 6px 0; } .device-name { font-weight: 500; color: var(--fg-color, #000); padding: 6px 0 0 0; } .device-entity { font-size: 10px; color: var(--fg-color, #000); font-family: monospace; } .device-details { font-size: 10px; color: var(--fg-color, #000); } .device-last-seen { font-size: 10px; color: var(--fg-color, #000); margin-left: auto; } .no-devices { text-align: center; padding: 8px 0 0 0; color: var(--fg-color, #000); } .loading { text-align: center; padding: 0px; color: var(--fg-color, #000); } /*加油图标样式*/ .device-details ha-icon { --mdc-icon-size: 12px; color: var(--fg-color, #000); } /*待办事项样式*/ .todo-item { transition: background-color 0.2s ease; } .todo-item:hover { background-color: rgba(150,150,150,0.1); border-radius: 4px; } .todo-item input[type="checkbox"] { cursor: pointer; } .todo-item button { background: none; border: none; cursor: pointer; padding: 4px; border-radius: 4px; transition: background-color 0.2s ease; } .todo-item button { color: #f44336; } .todo-item button:hover { background-color: rgba(244, 67, 54, 0.1); color: #d32f2f; } .add-todo { display: flex; gap: 4px; margin-top: 8px; } .add-todo input { flex: 1; padding: 4px; border-radius: 4px; background: var(--bg-color, #fff); border: 1px solid var(--fg-color, #000); color: var(--fg-color, #000); } .add-todo button { padding: 4px 8px; border-radius: 4px; border: 1px solid var(--fg-color, #000); background: var(--bg-color, #fff); color: var(--fg-color, #000); cursor: pointer; } .add-todo input:focus { outline: none; border-color: #2196F3; box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2); } .add-todo-expanded { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; padding: 8px; border: 1px solid var(--fg-color, #000); border-radius: 4px; background: var(--bg-color, #fff); } .add-todo-row { display: flex; gap: 8px; align-items: center; } .add-todo-description { flex: 1; padding: 4px; border: 1px solid var(--fg-color, #000); border-radius: 4px; background: var(--bg-color, #fff); color: var(--fg-color, #000); font-size: 13px; } .add-todo-description:focus { outline: none; border-color: #2196F3; box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2); } .add-todo-date { padding: 4px; border: 1px solid var(--fg-color, #000); border-radius: 4px; background: var(--bg-color, #fff); color: var(--fg-color, #000); font-size: 12px; width: 120px; } /* 确保日期输入框显示正确的格式 */ input[type="date"] { color-scheme: light dark; } input[type="date"]::-webkit-calendar-picker-indicator { cursor: pointer; filter: invert(0.5); } /* 深色主题下的日期选择器 */ [theme="off"] input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1); } .add-todo-toggle { background: none; border: 1px solid var(--fg-color, #000); color: var(--fg-color, #000); padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; margin-top: 8px; margin-bottom: 2px; } .add-todo-toggle:hover { background-color: rgba(33, 150, 243, 0.1); border-color: #2196F3; } .todo-content { flex: 1; display: flex; flex-direction: column; } .todo-main { display: flex; align-items: center; } .todo-due { color: #ff9800; font-size: 12px; margin-left: 4px; font-weight: 500; } .todo-description { color: #999; font-size: 11px; margin-top: 2px; line-height: 1.3; } .todo-item.no-description { align-items: center; } .todo-item.no-description input[type="checkbox"] { margin-top: 0; } .todo-item .edit-btn { background: none; border: none; cursor: pointer; padding: 4px; border-radius: 4px; transition: background-color 0.2s ease; color: #2196F3 !important; margin-right: 4px; } .todo-item .edit-btn:hover { background-color: rgba(33, 150, 243, 0.1); color: #1976D2 !important; } .edit-input { flex: 1; padding: 4px; border: 1px solid var(--fg-color, #000); border-radius: 4px; background: var(--bg-color, #fff); color: var(--fg-color, #000); font-size: 13px; margin-right: 8px; } .edit-input:focus { outline: none; border-color: #2196F3; box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2); } `; } constructor() { super(); this._todoData = []; this._loading = false; this._dataLoaded = false; //button新元素 this._refreshInterval = null; this.theme = 'on'; this._editingItem = null; this._expandedAddForm = {}; } static getConfigElement() { return document.createElement("xiaoshi-todo-button-editor"); } connectedCallback() { super.connectedCallback(); this._loadTodoData(); // 设置主题属性 this.setAttribute('theme', this._evaluateTheme()); // 每300秒刷新一次数据,减少频繁刷新 this._refreshInterval = setInterval(() => { this._loadTodoData(); }, 3000); } _evaluateTheme() { try { if (!this.config || !this.config.theme) return 'on'; if (typeof this.config.theme === 'function') { return this.config.theme(); } if (typeof this.config.theme === 'string' && (this.config.theme.includes('return') || this.config.theme.includes('=>'))) { return (new Function(`return ${this.config.theme}`))(); } return this.config.theme; } catch(e) { console.error('计算主题时出错:', e); return 'on'; } } _formatDate(dateString) { if (!dateString) return ''; const date = new Date(dateString); // 检查日期是否有效 if (isNaN(date.getTime())) { return dateString; // 如果无法解析,返回原字符串 } const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } _formatDateForInput(dateString) { if (!dateString) return ''; const date = new Date(dateString); // 检查日期是否有效 if (isNaN(date.getTime())) { return ''; } const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } _calculateDueDate(dueDate) { if (!dueDate) return ''; const today = new Date(); today.setHours(0, 0, 0, 0); const due = new Date(dueDate); // 检查日期是否有效 if (isNaN(due.getTime())) { return dueDate; // 如果无法解析,返回原字符串 } due.setHours(0, 0, 0, 0); const diffTime = due - today; const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); if (diffDays === 0) { return '今天'; } else if (diffDays === 1) { return '明天'; } else if (diffDays === -1) { return '昨天'; } else if (diffDays > 0 && diffDays <= 7) { return `${diffDays}天后`; } else if (diffDays > 7) { return this._formatDate(dueDate); } else { return `${Math.abs(diffDays)}天前`; } } disconnectedCallback() { super.disconnectedCallback(); if (this._refreshInterval) { clearInterval(this._refreshInterval); } } async _loadTodoData() { if (!this.hass) return; // button新元素 开始 删除下面 // this._loading = true; // this.requestUpdate(); // button新元素 介素 删除下面 try { const entities = this.config.entities || []; const todoData = []; for (const entityId of entities) { const entity = this.hass.states[entityId]; if (!entity) continue; // 获取待办事项项目 let todoItems = []; try { // 获取所有待办事项 - 使用 WebSocket API const result = await this.hass.callWS({ type: 'todo/item/list', entity_id: entityId }); todoItems = result.items || []; } catch (error) { console.error(`获取待办事项失败 ${entityId}:`, error); } const attributes = entity.attributes; todoData.push({ entity_id: entityId, friendly_name: attributes.friendly_name || entityId, icon: attributes.icon || 'mdi:format-list-checks', state: entity.state || '0', items: todoItems, incomplete_count: todoItems.filter(item => item.status === 'needs_action').length, completed_count: todoItems.filter(item => item.status === 'completed').length }); } this._todoData = todoData; this._dataLoaded = true; //button新元素 } catch (error) { console.error('加载待办事项数据失败:', error); this._todoData = []; this._dataLoaded = true; //button新元素 } this._loading = false; } _handleRefresh() { this._handleClick(); this._loadTodoData(); } _handleEntityClick(entity) { this._handleClick(); // 点击实体时打开实体详情页 if (entity.entity_id) { const evt = new Event('hass-more-info', { composed: true }); evt.detail = { entityId: entity.entity_id }; this.dispatchEvent(evt); } } _handleClick(){ if (navigator.vibrate) { navigator.vibrate(50); } else if (navigator.webkitVibrate) { navigator.webkitVibrate(50); } else { } } /*button新元素 开始*/ _handleButtonClick() { const tapAction = this.config.tap_action; if (!tapAction || tapAction !== 'none') { // 默认 tap_action 行为:弹出垂直堆叠卡片 const excludedParams = ['type', 'button_height', 'button_width', 'button_font_size', 'button_icon_size', 'show_preview', 'tap_action']; // 构建垂直堆叠卡片的内容 const cards = []; // 1. 添加待办信息卡片 const todoCardConfig = {}; Object.keys(this.config).forEach(key => { if (!excludedParams.includes(key) && key !== 'other_cards' && key !== 'no_preview') { todoCardConfig[key] = this.config[key]; } }); cards.push({ type: 'custom:xiaoshi-todo-card', ...todoCardConfig }); // 2. 添加附加卡片 if (this.config.other_cards && this.config.other_cards.trim()) { try { const additionalCardsConfig = this._parseYamlCards(this.config.other_cards); // 为每个附加卡片传递 theme 值 const cardsWithTheme = additionalCardsConfig.map(card => { // 如果卡片没有 theme 配置,则从当前卡片配置中传递 if (!card.theme && this.config.theme) { return { ...card, theme: this.config.theme }; } return card; }); cards.push(...cardsWithTheme); } catch (error) { console.error('解析附加卡片配置失败:', error); } } // 创建垂直堆叠卡片 const popupContent = { type: 'vertical-stack', cards: cards }; const popupStyle = this.config.popup_style || ` --mdc-theme-surface: rgb(0,0,0,0); --dialog-backdrop-filter: blur(10px) brightness(1); `; if (window.browser_mod) { window.browser_mod.service('popup', { style: popupStyle, content: popupContent }); } else { console.warn('browser_mod not available, cannot show popup'); } } this._handleClick(); } _parseYamlCards(yamlString) { try { const lines = yamlString.split('\n'); const cards = []; let currentCard = null; let indentStack = []; let contextStack = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const indentLevel = line.length - line.trimStart().length; if (trimmed.startsWith('- type')) { if (currentCard) { cards.push(currentCard); currentCard = null; indentStack = []; contextStack = []; } const content = trimmed.substring(1).trim(); if (content.includes(':')) { const [key, ...valueParts] = content.split(':'); const value = valueParts.join(':').trim(); currentCard = {}; this._setNestedValue(currentCard, key.trim(), this._parseValue(value)); } else { currentCard = { type: content }; } indentStack = [indentLevel]; contextStack = [currentCard]; } else if (currentCard && trimmed.startsWith('-')) { while (indentStack.length > 1 && indentLevel <= indentStack[indentStack.length - 1]) { indentStack.pop(); contextStack.pop(); } let currentContext = contextStack[contextStack.length - 1]; const itemValue = trimmed.substring(1).trim(); if (!Array.isArray(currentContext)) { if (contextStack.length > 1) { const parentContext = contextStack[contextStack.length - 2]; for (let key in parentContext) { if (parentContext[key] === currentContext) { parentContext[key] = []; contextStack[contextStack.length - 1] = parentContext[key]; currentContext = parentContext[key]; break; } } } } if (Array.isArray(currentContext)) { if (itemValue.includes(':')) { const [key, ...valueParts] = itemValue.split(':'); const value = valueParts.join(':').trim(); const obj = {}; obj[key.trim()] = this._parseValue(value); currentContext.push(obj); } else { currentContext.push(this._parseValue(itemValue)); } } } else if (currentCard && trimmed.includes(':')) { const [key, ...valueParts] = trimmed.split(':'); const value = valueParts.join(':').trim(); const keyName = key.trim(); while (indentStack.length > 1 && indentLevel <= indentStack[indentStack.length - 1]) { indentStack.pop(); contextStack.pop(); } const currentContext = contextStack[contextStack.length - 1]; if (value) { this._setNestedValue(currentContext, keyName, this._parseValue(value)); } else { let nextLine = null, nextIndent = null; for (let j = i + 1; j < lines.length; j++) { const nextTrimmed = lines[j].trim(); if (nextTrimmed && !nextTrimmed.startsWith('#')) { nextLine = nextTrimmed; nextIndent = lines[j].length - lines[j].trimStart().length; break; } } currentContext[keyName] = (nextLine && nextLine.startsWith('-') && nextIndent > indentLevel) ? [] : (currentContext[keyName] || {}); indentStack.push(indentLevel); contextStack.push(currentContext[keyName]); } } } if (currentCard) cards.push(currentCard); return cards; } catch (error) { console.error('YAML解析错误:', error); return []; } } _parseValue(value) { if (!value) return ''; // 移除引号 if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { return value.slice(1, -1); } // 尝试解析为数字 if (!isNaN(value) && value.trim() !== '') { return Number(value); } // 尝试解析为布尔值 if (value === 'true') return true; if (value === 'false') return false; if (value === 'null') return null; // 返回字符串 return value; } _setNestedValue(obj, path, value) { // 支持嵌套路径,如 "styles.card" const keys = path.split('.'); let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!current[key] || typeof current[key] !== 'object') { current[key] = {}; } current = current[key]; } current[keys[keys.length - 1]] = value; } /*button新元素 结束*/ async _addTodoItem(entityId, item, description = '', due = '') { try { const params = { entity_id: entityId, item: item }; // 只有当描述不为空时才添加 if (description && description.trim()) { params.description = description.trim(); } // 只有当日期不为空时才添加 if (due && due.trim()) { params.due_date = due.trim(); } await this.hass.callService('todo', 'add_item', params); this._loadTodoData(); // 重新加载数据 } catch (error) { console.error('添加待办事项失败:', error); } } async _removeTodoItem(entityId, item) { try { await this.hass.callService('todo', 'remove_item', { entity_id: entityId, item: item }); this._loadTodoData(); // 重新加载数据 } catch (error) { console.error('删除待办事项失败:', error); } } async _updateTodoItem(entityId, item, status) { try { await this.hass.callService('todo', 'update_item', { entity_id: entityId, item: item, status: status }); this._loadTodoData(); // 重新加载数据 } catch (error) { console.error('更新待办事项失败:', error); } } async _editTodoItem(entityId, oldItem, newItem, description = '', due = '') { try { // 先删除旧的待办事项,然后添加新的 await this.hass.callService('todo', 'remove_item', { entity_id: entityId, item: oldItem }); const params = { entity_id: entityId, item: newItem }; // 只有当描述不为空时才添加 if (description && description.trim()) { params.description = description.trim(); } // 只有当日期不为空时才添加 if (due && due.trim()) { params.due_date = due.trim(); } await this.hass.callService('todo', 'add_item', params); this._loadTodoData(); // 重新加载数据 } catch (error) { console.error('修改待办事项失败:', error); } } render() { if (!this.hass) { return html`
等待Home Assistant连接...
`; } // 获取主题和颜色 const theme = this._evaluateTheme(); const fgColor = theme === 'on' ? 'rgb(0, 0, 0)' : 'rgb(255, 255, 255)'; const bgColor = theme === 'on' ? 'rgb(255, 255, 255)' : 'rgb(50, 50, 50)'; // 计算总待办数量 const totalIncompleteCount = this._todoData.reduce((sum, todo) => sum + todo.incomplete_count, 0); /*button新元素 前9行和最后1行开始*/ const showPreview = this.config.no_preview === true; // 获取新参数 const badgeMode = this.config.badge_mode === true; const transparentBg = this.config.transparent_bg === true; const hideIcon = this.config.hide_icon === true; const hideColon = this.config.hide_colon === true; const hideZero = this.config.hide_zero === true; const autoHide = this.config.auto_hide === true; const lockWhiteFg = this.config.lock_white_fg === true; const buttonText = this.config.button_text || '待办'; const buttonIcon = this.config.button_icon || 'mdi:clipboard-list'; // 设置背景颜色 const buttonBgColor = transparentBg ? 'transparent' : theme === 'on' ? 'rgb(255, 255, 255, 0.6)' : 'rgb(83, 83, 83, 0.6)'; // 检查是否需要自动隐藏(只有数据加载完成且数量为0时才考虑隐藏) const shouldAutoHide = this._dataLoaded && autoHide && totalIncompleteCount === 0; // 如果需要自动隐藏,返回空div if (shouldAutoHide) { return html`
`; } // 渲染按钮 let buttonHtml; if (!this._dataLoaded) { if (badgeMode) { // 角标模式:只显示图标,数量为0时不显示角标 buttonHtml = html`
`; } else { // 普通模式 // 应用锁定白色功能 const iconColor = lockWhiteFg ? 'rgb(255, 255, 255)' : fgColor; const textColor = lockWhiteFg ? 'rgb(255, 255, 255)' : fgColor; // 构建显示文本 let displayText = buttonText; // 根据hide_colon参数决定是否显示冒号 if (!hideColon) { displayText += ':'; } else { displayText += ' '; } // 根据hide_zero参数决定是否显示0值 if (!hideZero) { displayText += ' 0'; } else { // 隐藏0值时使用CSS空格占位符,保持布局稳定 displayText += '\u2002'; // 两个en空格,大约等于数字"0"的宽度 } buttonHtml = html`
${!hideIcon ? html`` : ''} ${displayText}
`; } } else { // 数据加载完成后 if (badgeMode) { // 角标模式:只显示图标,根据数量显示角标 const hasWarning = totalIncompleteCount > 0; buttonHtml = html`
${hasWarning ? html`
${totalIncompleteCount}
` : ''}
`; } else { // 普通模式:显示文本和数量 // 应用锁定白色功能,但预警颜色(红色)不受影响 let textColor, iconColor; if (totalIncompleteCount === 0) { // 非预警状态:根据锁定白色设置决定颜色 textColor = lockWhiteFg ? 'rgb(255, 255, 255)' : fgColor; iconColor = lockWhiteFg ? 'rgb(255, 255, 255)' : fgColor; } else { // 预警状态:始终使用红色,不受锁定白色影响 textColor = 'rgb(255, 0, 0)'; iconColor = lockWhiteFg ? 'rgb(255, 255, 255)' : fgColor; } // 构建显示文本 let displayText = buttonText; // 根据hide_colon参数决定是否显示冒号 if (!hideColon) { displayText += ':'; } else { displayText += ' '; } // 根据hide_zero参数和实际数量决定是否显示数量 if (hideZero && totalIncompleteCount === 0) { // 隐藏0值时使用CSS空格占位符,保持布局稳定 displayText += '\u2002'; // 两个en空格,大约等于数字"0"的宽度 } else { displayText += ` ${totalIncompleteCount}`; } buttonHtml = html`
${!hideIcon ? html`` : ''} ${displayText}
`; } } // 返回最终的渲染结果(包括按钮和预览卡片) return html` ${buttonHtml} ${showPreview ? html`
待办事项
${totalIncompleteCount}
${this._loading ? html`
加载中...
` : this._todoData.length === 0 ? html`
请配置待办事项实体
` : html` ${this._todoData.map(todoData => html`
${todoData.friendly_name} ${todoData.incomplete_count}
${todoData.items.length === 0 ? html`
暂无待办事项
` : html` ${(() => { // 将待办事项分为有时间和无时间两组 const itemsWithoutTime = todoData.items.filter(item => !item.due); const itemsWithTime = todoData.items.filter(item => item.due); // 没有时间的按名称排序 itemsWithoutTime.sort((a, b) => (a.summary || '').localeCompare(b.summary || '')); // 有时间的按时间排序 itemsWithTime.sort((a, b) => { const dateA = new Date(a.due); const dateB = new Date(b.due); return dateA - dateB; }); // 合并结果:无时间的在前,有时间的在后 const sortedItems = [...itemsWithoutTime, ...itemsWithTime]; return sortedItems.map(item => { const dueText = this._calculateDueDate(item.due); const isEditing = this._editingItem && this._editingItem.entityId === todoData.entity_id && this._editingItem.uid === item.uid; return html`
{ this._updateTodoItem(todoData.entity_id, item.summary || item.uid, e.target.checked ? 'completed' : 'needs_action'); this._handleClick(); }} style="margin-right: 8px; margin-top: 2px;" /> ${isEditing ? html`
{ this._editingItem.summary = e.target.value; this.requestUpdate(); }} />
{ this._editingItem.due = e.target.value; this.requestUpdate(); this._handleClick(); }} />
` : html`
${item.summary} ${dueText ? html`(${dueText})` : ''}
${item.description ? html`
${item.description}
` : ''}
`} ${!isEditing ? html` ` : ''}
`; }); })()} ` }
${this._expandedAddForm[todoData.entity_id] ? html`
{ if (e.key === 'Enter') { e.preventDefault(); const descInput = e.target.parentElement.querySelector('.add-todo-description:nth-of-type(2)'); const dateInput = e.target.parentElement.querySelector('.add-todo-date'); if (e.target.value.trim()) { this._addTodoItem(todoData.entity_id, e.target.value.trim(), descInput.value, dateInput.value); e.target.value = ''; descInput.value = ''; dateInput.value = ''; } } }} />
` : ''}
`)} ` }
` : html``} `; /*button新元素 结束*/ } setConfig(config) { /*button新元素 开始*/ // 不设置默认值,只有明确配置时才添加 no_preview this.config = { ...config }; if (config.button_width) { this.style.setProperty('--button-width', config.button_width); } else { this.style.setProperty('--button-width', '65px'); } // 设置按钮高度(只控制 todo-status) if (config.button_height) { this.style.setProperty('--button-height', config.button_height); } else { this.style.setProperty('--button-height', '24px'); } // 设置按钮文字大小(只控制 todo-status) if (config.button_font_size) { this.style.setProperty('--button-font-size', config.button_font_size); } else { this.style.setProperty('--button-font-size', '11px'); } // 设置按钮图标大小(只控制 todo-status) if (config.button_icon_size) { this.style.setProperty('--button-icon-size', config.button_icon_size); } else { this.style.setProperty('--button-icon-size', '13px'); } // 设置卡片宽度(控制原来的 UI) if (config.width) { this.style.setProperty('--card-width', config.width); } else { this.style.setProperty('--card-width', '100%'); } /*button新元素 结束*/ // 设置主题 if (config.theme) { this.setAttribute('theme', config.theme); } } getCardSize() { // 根据待办事项实体数量动态计算卡片大小 const baseSize = 3; const entitySize = Math.max(0, Math.min(this._todoData.length * 2, 10)); return baseSize + entitySize; } } customElements.define('xiaoshi-todo-button', XiaoshiTodoButton);