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