Greasy Fork

Greasy Fork is available in English.

米家极客版油猴插件

在极客页面中,点击设备列表,调用API获取设备和规则列表,并生成设备规则映射并显示在当前页面上

当前为 2024-05-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         米家极客版油猴插件
// @namespace    http://tampermonkey.net/
// @version      v0.8
// @description  在极客页面中,点击设备列表,调用API获取设备和规则列表,并生成设备规则映射并显示在当前页面上
// @author       王丰,sk163
// @license      MIT
// @match        http://*/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==
(async () => {
    const callAPI = (api, params) => {
        return new Promise(res => editor.gateway.callAPI(api, params, res));
    };
    let selectCardIds = '';
    let defaultColor='#43ad7f7f'
    let backgroundColor = GM_getValue("backgroundColor") ?? '';
    let windowWidth = GM_getValue("windowWidth") ?? 800;
    let windowHeight= GM_getValue("windowHeight") ?? 600;


    const executeScript = async () => {
        if (document.getElementById('device-rule-map')) {
            return;
        }

        if (typeof editor === 'undefined' || typeof editor.gateway === 'undefined' || typeof editor.gateway.callAPI === 'undefined') {
            console.error('editor.gateway.callAPI 方法未定义。请确保在正确的环境中运行此脚本。');
            return;
        }

        try {
            const devListResponse = await callAPI('getDevList');
            const devList = devListResponse.devList;
            const ruleList = await callAPI('getGraphList');
            let devRuleMap = {};

            for (const rule of ruleList) {
                const content = await callAPI('getGraph', {id: rule.id});
                const dids = new Set(content.nodes.map(n => n.props?.did).filter(d => d !== undefined));
                const cards = new Set(content.nodes.map(n => {
                    return (n.props && n.cfg) ? {did: n.props.did, oriId: n.cfg.oriId} : undefined;
                }).filter(d => d !== undefined));

                dids.forEach(d => {
                    devRuleMap[d] = devRuleMap[d] ?? [];
                    const cardIds = Array.from(cards)
                        .filter(card => card.did === d)
                        .map(card => card.oriId).join(',');
                    const tempDevRule = {
                        ruleId: rule.id,
                        cardIds: cardIds,
                        totalCardNum: cards.size
                    };

                    devRuleMap[d].push(tempDevRule);
                });
            }

            const result = Object.fromEntries(Object.entries(devRuleMap).map(([k, v]) => [
                devList[k]?.name ?? `did: ${k}`,
                v.map(r => {
                    return ({
                        id: r.ruleId,
                        cardIds: r.cardIds,
                        totalCardNum: r.totalCardNum,
                        name: ruleList.find(rr => rr.id === r.ruleId).userData.name
                    });
                })
            ]));

            const container = document.createElement('div');
            container.id = 'device-rule-map';
            container.style.position = 'fixed';
            container.style.top = '10px';
            container.style.right = '10px';
            container.style.width = windowWidth+'px';
            container.style.height = windowHeight+'px';
            container.style.overflowY = 'scroll';
            container.style.backgroundColor = 'white';
            container.style.border = '1px solid #ccc';
            container.style.paddingTop = '50px';
            container.style.zIndex = 10000;
            container.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';

            const topBar = document.createElement('div');
            topBar.style.position = 'fixed';
            topBar.style.top = '0';
            topBar.style.right = '10px';
            topBar.style.width = windowWidth+'px';
            topBar.style.height = '50px';
            topBar.style.backgroundColor = 'white';
            topBar.style.zIndex = 10001;
            topBar.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
            topBar.style.display = 'flex';
            topBar.style.justifyContent = 'space-between';
            topBar.style.alignItems = 'center';
            topBar.style.padding = '0 10px';

            const title = document.createElement('h1');
            title.style.margin = '0';
            title.textContent = '设备规则映射结果';

            const buttonContainer = document.createElement('div');
            buttonContainer.style.display = 'flex';
            buttonContainer.style.gap = '10px';

            const collapseButton = document.createElement('button');
            collapseButton.textContent = '折叠';
            collapseButton.onclick = () => {
                if (container.style.height === windowHeight+'px') {
                    topBar.style.width = '350px';
                    container.style.height = '50px';
                    container.style.width = '50px';
                    collapseButton.textContent = '展开';
                } else {
                    topBar.style.width = windowWidth+'px';
                    container.style.width = windowWidth+'px';
                    container.style.height = windowHeight+'px';
                    collapseButton.textContent = '折叠';
                }
            };

            const closeButton = document.createElement('button');
            closeButton.textContent = '关闭';
            closeButton.onclick = () => document.body.removeChild(container);

            buttonContainer.appendChild(collapseButton);
            buttonContainer.appendChild(closeButton);

            topBar.appendChild(title);
            topBar.appendChild(buttonContainer);

            const table = document.createElement('table');
            table.border = '1';
            table.cellSpacing = '0';
            table.cellPadding = '5';
            table.style.width = '100%';

            const thead = document.createElement('thead');
            const headerRow = document.createElement('tr');
            const deviceHeader = document.createElement('th');
            const ruleHeader = document.createElement('th');

            let deviceSortOrder = 'asc';
            let ruleSortOrder = 'asc';

            const updateSortMarkers = () => {
                deviceHeader.innerHTML = `设备 ${deviceSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
                ruleHeader.innerHTML = `规则 ${ruleSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
            };

            deviceHeader.textContent = '设备';
            ruleHeader.textContent = '规则';

            deviceHeader.onclick = () => {
                deviceSortOrder = deviceSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(0, deviceSortOrder);
                updateSortMarkers();
            };
            ruleHeader.onclick = () => {
                ruleSortOrder = ruleSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(1, ruleSortOrder);
                updateSortMarkers();
            };

            headerRow.appendChild(deviceHeader);
            headerRow.appendChild(ruleHeader);
            thead.appendChild(headerRow);
            table.appendChild(thead);

            const deviceFilterInput = document.createElement('input');
            deviceFilterInput.type = 'text';
            deviceFilterInput.placeholder = '设备筛选';
            deviceFilterInput.style.marginBottom = '10px';
            deviceFilterInput.oninput = () => {
                filterTable(deviceFilterInput.value, ruleFilterInput.value);
            };
            container.appendChild(deviceFilterInput);

            const ruleFilterInput = document.createElement('input');
            ruleFilterInput.type = 'text';
            ruleFilterInput.placeholder = '规则筛选';
            ruleFilterInput.style.marginBottom = '10px';
            ruleFilterInput.style.marginLeft = '10px';
            ruleFilterInput.oninput = () => {
                filterTable(deviceFilterInput.value, ruleFilterInput.value);
            };
            container.appendChild(ruleFilterInput);

            const widthInput = document.createElement('input');
            widthInput.type = 'text';
            widthInput.placeholder = windowWidth+'px';
            widthInput.style.width = '80px';
            widthInput.style.marginBottom = '10px';
            widthInput.style.marginLeft = '10px';
            widthInput.oninput = () => {
                windowWidth=widthInput.value;
                GM_setValue("windowWidth", windowWidth);
                container.style.width = windowWidth + 'px';
                topBar.style.width = windowWidth + 'px';
            };
            const spanW = document.createElement('span');
            spanW.textContent = '窗口宽度:';
            spanW.style.marginLeft = '10px';
            container.appendChild(spanW);
            container.appendChild(widthInput);

            const heightInput = document.createElement('input');
            heightInput.type = 'text';
            heightInput.placeholder = windowHeight+'px';
            heightInput.style.width = '80px';
            heightInput.style.marginBottom = '10px';
            heightInput.style.marginLeft = '10px';
            heightInput.oninput = () => {
                windowHeight=heightInput.value;
                GM_setValue("windowHeight", windowHeight);
                container.style.height = windowHeight + 'px';
            };
            const spanH = document.createElement('span');
            spanH.textContent = '窗口高度:';
            spanH.style.marginLeft = '10px';
            container.appendChild(spanH);
            container.appendChild(heightInput);

            const colorInput = document.createElement('input');
            colorInput.type = 'text';
            colorInput.placeholder=defaultColor;
            colorInput.style.width = '80px';
            colorInput.style.marginBottom = '10px';
            colorInput.style.marginLeft = '10px';
            colorInput.oninput = () => {
                backgroundColor = colorInput.value;
                GM_setValue("backgroundColor", backgroundColor);
            };
            const spanC = document.createElement('span');
            spanC.textContent = '高亮颜色:';
            spanC.style.marginLeft = '10px';
            container.appendChild(spanC);
            container.appendChild(colorInput);

            const tbody = document.createElement('tbody');
            Object.entries(result).forEach(([device, rules]) => {
                const row = document.createElement('tr');
                const deviceCell = document.createElement('td');
                deviceCell.textContent = device;
                const ruleCell = document.createElement('td');

                const host = window.location.host;

                rules.forEach(rule => {
                    const link = document.createElement('a');
                    link.href = `http://${host}/#/graph/${rule.id}`;
                    link.target = '_self';
                    link.textContent = rule.name + "[" + rule.cardIds.split(',').length + "/" + rule.totalCardNum + "]";
                    link.onclick = () => {
                        window.location.hash = '#/';
                        selectCardIds = rule.cardIds;
                    };
                    ruleCell.appendChild(link);
                    ruleCell.appendChild(document.createTextNode(', '));
                });
                ruleCell.removeChild(ruleCell.lastChild);
                row.appendChild(deviceCell);
                row.appendChild(ruleCell);
                tbody.appendChild(row);
            });
            table.appendChild(tbody);

            container.appendChild(topBar);
            container.appendChild(table);
            document.body.appendChild(container);

            function sortTable(columnIndex, sortOrder) {
                const rows = Array.from(tbody.rows);
                const sortedRows = rows.sort((a, b) => {
                    const aText = a.cells[columnIndex].textContent;
                    const bText = b.cells[columnIndex].textContent;
                    if (sortOrder === 'asc') {
                        return aText.localeCompare(bText);
                    } else {
                        return bText.localeCompare(aText);
                    }
                });
                tbody.innerHTML = '';
                sortedRows.forEach(row => tbody.appendChild(row));
            }

            updateSortMarkers();

            function filterTable(deviceKeyword, ruleKeyword) {
                const rows = Array.from(tbody.rows);
                rows.forEach(row => {
                    const deviceText = row.cells[0].textContent.toLowerCase();
                    const ruleText = row.cells[1].textContent.toLowerCase();
                    if (deviceText.includes(deviceKeyword.toLowerCase()) && ruleText.includes(ruleKeyword.toLowerCase())) {
                        row.style.display = '';
                    } else {
                        row.style.display = 'none';
                    }
                });
            }

        } catch (error) {
            console.error('调用 API 时出错:', error);
        }
    };

    const selectDevices = async () => {
        await sleep(1000);
        const cardIds = selectCardIds.split(',');
        for (const cardId of cardIds) {
            if (cardId.trim() !== '') {
                let targetElement = document.querySelector("#" + cardId.trim() + " > div > div");
                if (targetElement) {
                    targetElement.style.backgroundColor = backgroundColor === '' ? defaultColor : backgroundColor;
                }
            }
        }
        selectCardIds = '';

        function sleep(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        }
    };

    function isMiJiaJiKePage() {
        return document.title === "米家自动化极客版";
    }

    if (isMiJiaJiKePage() && window.location.hash === '#/device') {
        executeScript();
    }

    window.addEventListener('hashchange', () => {
        if (isMiJiaJiKePage() && window.location.hash === '#/device') {
            executeScript();
        }
        if (isMiJiaJiKePage() && window.location.hash.match(/^#\/graph\/.*/g)) {
            selectDevices();
        }
    });
})();