diff --git a/xiaoshi-pad-card.js b/xiaoshi-pad-card.js new file mode 100644 index 0000000..677b5b3 --- /dev/null +++ b/xiaoshi-pad-card.js @@ -0,0 +1,24 @@ +console.info("%c 消逝卡-平板端 \n%c v 0.0.0 ", "color: red; font-weight: bold; background: black", "color: white; font-weight: bold; background: black"); + +const loadCards = async () => { + await import('./xiaoshi-pad-grid-card.js'); + await import('./xiaoshi-pad-slider-card.js'); + + window.customCards = window.customCards || []; + window.customCards.push(...cardConfigs); +}; + +const cardConfigs = [ + { + type: 'xiaoshi-pad-grid-card', + name: '消逝卡(平板端)-分布卡', + description: '温度分布、湿度分布' + }, + { + type: 'xiaoshi-pad-slider-card', + name: '消逝卡(平板端)-进度条', + description: '进度条' + } +]; + +loadCards(); \ No newline at end of file diff --git a/xiaoshi-pad-grid-card.js b/xiaoshi-pad-grid-card.js new file mode 100644 index 0000000..b16a18b --- /dev/null +++ b/xiaoshi-pad-grid-card.js @@ -0,0 +1,142 @@ +import { LitElement, html, css } from "https://unpkg.com/lit-element@2.4.0/lit-element.js?module"; + +export class XiaoshiPadGridCard extends LitElement { + static get properties() { + return { + hass: Object, + config: Object, + }; + } + + static get styles() { + return css` + .container { + position: relative; + display: block; + overflow: hidden; + } + .grid-item { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + color: white; + box-sizing: border-box; + border: 0; + cursor: pointer; + } + `; + } + + setConfig(config) { + if (!config.entities) { + throw new Error('You need to define entities'); + } + this.config = { + width: config.width || '400px', + height: config.height || '80px', + min: config.min || 0, + max: config.max || 100, + mode: config.mode || '温度', + display: config.display || false, + entities: config.entities.map(entity => ({ + ...entity, + state: entity.state !== false, + })), + }; + } + + render() { + if(this._display()) return; + return html` +
+ ${this.config.entities.map((entityConfig) => { + const entity = this.hass.states[entityConfig.entity]; + if (!entity) return html``; + const value = parseFloat(entity.state); + const grid = entityConfig.grid ? entityConfig.grid.split(',') : ['0%', '0%', '100%', '100%']; + const unit = entityConfig.unit || ''; + let filter; + if (this.config.mode === '温度') { + filter = this._calculateTemperatureFilter(value); + } else if (this.config.mode === '湿度') { + filter = this._calculateHumidityFilter(value); + }; + let size = Number(grid[2].slice(0, grid[2].length-1)); + let fsize ="11px"; + if (size<25 ) fsize ="10px"; + if (size<20 ) fsize ="9px"; + if (size<15 ) fsize ="8px"; + return html` +
+ ${entityConfig.state !== false ? html`${entity.state}${unit}` : ''} +
+ `; + })} +
+ `; + } + + _display() { + try { + if (this.config.display === undefined) return false; + if (typeof this.config.display === 'boolean') { + return this.config.display; + }; + if (typeof this.config.display === 'function') { + const result = this.config.display(); + return result === true || result === "true"; // 同时接受 true 和 "true" + }; + if (typeof this.config.display === 'string') { + const displayStr = this.config.display.trim(); + if (displayStr.startsWith('[[[') && displayStr.endsWith(']]]')) { + const funcBody = displayStr.slice(3, -3).trim(); + const result = new Function('states', funcBody)(this.hass.states); + return result === true || result === "true"; // 同时接受 true 和 "true" + } + if (displayStr.includes('return') || displayStr.includes('=>')) { + const result = (new Function(`return ${displayStr}`))(); + return result === true || result === "true"; + } + const result = (new Function(`return ${displayStr}`))(); + return result === true || result === "true"; + }; + return false; + } catch(e) { + console.error('显示出错:', e); + return false; + } + } + + _calculateTemperatureFilter(temp) { + temp = parseFloat(temp); + const { min, max } = this.config; + let deg; + if (temp > 25) { + deg = (25 - temp) * 120 / (max - 25); + } else { + deg = (25 - temp) * 100 / (25 - min); + }; + return `hue-rotate(${deg}deg)`; + } + + _calculateHumidityFilter(hum) { + hum = parseFloat(hum); + const { min, max } = this.config; + let deg; + if (hum > 50) { + deg = (50 - hum) * 100 / (50 - max); + } else { + deg = (50 - hum) * 120 / (min - 50); + }; + return `hue-rotate(${deg}deg)`; + } + + getCardSize() { + return 1; + } +} +customElements.define('xiaoshi-pad-grid-card', XiaoshiPadGridCard); diff --git a/xiaoshi-pad-slider-card.js b/xiaoshi-pad-slider-card.js new file mode 100644 index 0000000..f3c3352 --- /dev/null +++ b/xiaoshi-pad-slider-card.js @@ -0,0 +1,163 @@ +import { LitElement, html, css } from "https://unpkg.com/lit-element@2.4.0/lit-element.js?module"; + +export class XiaoshiPadSliderCard extends LitElement { + static get properties() { + return { + hass: Object, + config: Object, + _value: Number, + _min: Number, + _max: Number, + _dragging: Boolean + }; + } + + static get styles() { + return css` + .slider-root { + position: relative; + width: var(--slider-width, 100%); + height: var(--slider-height, 30px); + touch-action: none; + } + .slider-track { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 100%; + height: var(--track-height, 5px); + background: var(--track-color, rgba(255,255,255,0.3)); + border-radius: var(--track-radius, 2px); + } + .slider-fill { + position: absolute; + height: 100%; + background: var(--slider-color, #f00); + border-radius: inherit; + } + .slider-thumb { + position: absolute; + top: 50%; + width: var(--thumb-size, 15px); + height: var(--thumb-size, 15px); + background: var(--thumb-color, #fff); + border-radius: 50%; + transform: translate(-50%, -50%); + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + } + `; + } + + constructor() { + super(); + this._value = 0; + this._min = 0; + this._max = 100; + this._dragging = false; + this._startX = 0; + this._startValue = 0; + this._moveHandler = (e) => this._handleDrag(e); + this._endHandler = () => this._endDrag(); + } + + setConfig(config) { + if (!config.entity) throw new Error('必须指定实体'); + this.config = config; + if (config.style) { + Object.keys(config.style).forEach(key => { + this.style.setProperty(`--${key}`, config.style[key]); + }); + } + } + + updated(changedProperties) { + if (changedProperties.has('hass')) { + const state = this.hass.states[this.config.entity]; + if (state) { + this._value = Number(state.state); + this._min = Number(state.attributes.min || 0); + this._max = Number(state.attributes.max || 100); + } + } + } + + render() { + const percent = Math.max(0, Math.min(100, (this._value - this._min) / (this._max - this._min) * 100)); + return html` +
+
+
+
+
+
+ `; + } + + _startDrag(e) { + e.preventDefault(); + this._dragging = true; + const slider = this.shadowRoot.querySelector('.slider-track'); + const rect = slider.getBoundingClientRect(); + this._sliderLeft = rect.left; + this._sliderWidth = rect.width; + window.addEventListener('mousemove', this._moveHandler); + window.addEventListener('touchmove', this._moveHandler, { passive: false }); + window.addEventListener('mouseup', this._endHandler); + window.addEventListener('touchend', this._endHandler); + const clientX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX; + this._updateValue((clientX - this._sliderLeft) / this._sliderWidth); + } + + _endDrag() { + if (!this._dragging) return; + this._dragging = false; + this._removeEventListeners(); + } + + _handleDrag(e) { + if (!this._dragging) return; + e.preventDefault(); + const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX; + this._updateValue((clientX - this._sliderLeft) / this._sliderWidth); + } + + _removeEventListeners() { + window.removeEventListener('mousemove', this._moveHandler); + window.removeEventListener('touchmove', this._moveHandler); + window.removeEventListener('mouseup', this._endHandler); + window.removeEventListener('touchend', this._endHandler); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this._removeEventListeners(); + } + + _updateValue(ratio) { + const safeRatio = Math.max(0, Math.min(1, ratio)); + const newValue = this._min + safeRatio * (this._max - this._min); + const roundedValue = Math.round(newValue); + if (roundedValue !== this._value) { + this._value = roundedValue; + this._debouncedSetValue(roundedValue); + } + } + + _debouncedSetValue(value) { + clearTimeout(this._debounceTimer); + this._debounceTimer = setTimeout(() => { + this._callService(value); + }, 50); + } + + _callService(value) { + const service = this.config.entity.split('.')[0]; + this.hass.callService(service, 'set_value', { + entity_id: this.config.entity, + value: value + }); + } +} +customElements.define('xiaoshi-pad-slider-card', XiaoshiPadSliderCard); \ No newline at end of file