【油猴脚本】处理集团OA待办页面,批量打开且高亮待办标签

处理集团OA待办页面,批量打开且高亮待办标签。

// ==UserScript==
// @name         批量打开新OA待办(多标签自动复选+高亮+全选+半选+对齐修复)
// @namespace    http://tampermonkey.net/
// @version      5.4
// @description  高亮“省内待办(X)”,行复选框可选,表头复选框全选/反选/半选;防止表头触发排序;批量打开后恢复全选;切换标签页自动生效;修复表头与行复选框精确对齐。
// @match        http://XXXX/backlog/cmit/web/index/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // --------------------------
    // 样式:统一基准到 .cell
    // --------------------------
    const style = document.createElement('style');
    style.textContent = `
        .todo-tab-highlight {
            color: #e60000 !important;
            font-weight: bold !important;
            background-color: #fff1f0 !important;
            padding: 4px 10px !important;
            border-radius: 6px !important;
            border: 1px solid #ff4d4f !important;
            display: inline-block !important;
        }
        .todo-num {
            color: #e60000 !important;
            font-weight: bold !important;
        }

        /* 统一把复选框基准设在 .cell(表头与每行的 cell)上 */
        .el-table__header-wrapper thead tr th .cell.title,
        .el-table__body tr.el-table__row td .cell {
            position: relative !important;
            /* 给复选框和文字留出相同的左侧空隙 */
            padding-left: 36px !important;
        }

        /* 表头复选框与行复选框采用相同的绝对定位 */
        .header-checkbox,
        .todo-checkbox {
            position: absolute !important;
            left: 8px !important; /* 复选框距离 cell 左侧的偏移(可微调) */
            top: 50% !important;
            transform: translateY(-50%) !important;
            margin: 0 !important;
            cursor: pointer;
            box-sizing: border-box !important;
        }

        /* 避免影响其它列,限制只针对第一列(可增强稳健性) */
        .el-table__header-wrapper thead tr th.el-table_1_column_1 .cell.title,
        .el-table__body tr.el-table__row td.el-table_1_column_1 .cell {
            padding-left: 36px !important;
        }

        /* 按钮样式保持不变 */
        #bulkOpenBtn {
            position: fixed;
            left: 10px;
            top: 50%;
            transform: translateY(-50%);
            z-index: 9999;
            padding: 10px 16px;
            background-color: #409EFF;
            color: #fff;
            border: none;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.2);
            font-size: 14px;
            cursor: pointer;
        }
        #bulkOpenBtn:hover {
            background-color: #66b1ff;
            transform: translateY(-50%) scale(1.05);
        }
    `;
    document.head.appendChild(style);

    // --------------------------
    // 基础工具函数(保持不变)
    // --------------------------
    function waitForElement(selector, timeout = 10000) {
        return new Promise((resolve, reject) => {
            const el = document.querySelector(selector);
            if (el) return resolve(el);
            const obs = new MutationObserver(() => {
                const target = document.querySelector(selector);
                if (target) {
                    obs.disconnect();
                    resolve(target);
                }
            });
            obs.observe(document.body, { childList: true, subtree: true });
            setTimeout(() => {
                obs.disconnect();
                reject(new Error(`Timeout waiting for element: ${selector}`));
            }, timeout);
        });
    }

    // 高亮标签(保持你之前的逻辑)
    function highlightTodoTabs() {
        document.querySelectorAll('.todo-num').forEach(numEl => {
            const match = (numEl.textContent || '').trim().match(/\d+/);
            const num = match ? parseInt(match[0], 10) : 0;
            const tabText = numEl.closest('.text');
            if (!tabText) return;

            if (num > 0) {
                tabText.classList.add('todo-tab-highlight');
                numEl.classList.add('todo-num');
            } else {
                tabText.classList.remove('todo-tab-highlight');
                tabText.removeAttribute('style');
                numEl.classList.remove('todo-num');
                numEl.removeAttribute('style');
            }
        });
    }

    // --------------------------
    // 行复选框:插入到 .cell(与表头同级)以保证对齐
    // --------------------------
    function addCheckboxes() {
        const rows = document.querySelectorAll('.el-table__body tr.el-table__Row, .el-table__body tr.el-table__row');
        rows.forEach(row => {
            // 找到行内的标题 span(原来的位置)
            const titleSpan = row.querySelector('.itemTitle .item-title--click') || row.querySelector('.item-title--click');
            if (!titleSpan) return;

            // 找到对应的 .cell 容器(我们将复选框插到这个 cell 中)
            const cellDiv = titleSpan.closest('.cell');
            if (!cellDiv) return;

            // 若 cell 已有复选框,则跳过
            if (cellDiv.classList.contains('tm-has-checkbox')) return;
            if (cellDiv.querySelector('.todo-checkbox')) {
                cellDiv.classList.add('tm-has-checkbox');
                return;
            }

            // 标记防重复
            cellDiv.classList.add('tm-has-checkbox');

            // 创建复选框并插入到 cell 的最前面(不移动原始 link 内容)
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.className = 'todo-checkbox';
            checkbox.checked = true;

            // 阻止冒泡(避免触发行点击)
            checkbox.addEventListener('click', e => e.stopPropagation());
            checkbox.addEventListener('mousedown', e => e.stopPropagation());
            checkbox.addEventListener('change', updateHeaderCheckbox);

            // 插入 cell 的第一个子节点(保持 link 在原地,没有被移动)
            cellDiv.insertBefore(checkbox, cellDiv.firstChild);
        });
    }

    // --------------------------
    // 表头复选框(保持原逻辑,但位置和定位一致)
    // --------------------------
    function addHeaderCheckbox() {
        const headerCell = document.querySelector('.el-table__header-wrapper thead tr th .cell.title');
        if (!headerCell) return;
        if (headerCell.querySelector('.header-checkbox')) return;

        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.className = 'header-checkbox';

        // 阻止冒泡,防止触发列排序
        checkbox.addEventListener('click', e => e.stopPropagation());
        checkbox.addEventListener('mousedown', e => e.stopPropagation());

        checkbox.addEventListener('change', function () {
            const rowCheckboxes = document.querySelectorAll('.todo-checkbox');
            rowCheckboxes.forEach(cb => cb.checked = this.checked);
            checkbox.indeterminate = false;
        });

        // 插入到 .cell.title 的最前面(与行的复选框插入点一致)
        headerCell.insertBefore(checkbox, headerCell.firstChild);

        // 默认全选并清除半选
        const rowCheckboxes = document.querySelectorAll('.todo-checkbox');
        rowCheckboxes.forEach(cb => cb.checked = true);
        checkbox.checked = true;
        checkbox.indeterminate = false;
    }

    function updateHeaderCheckbox() {
        const header = document.querySelector('.header-checkbox');
        const rowCheckboxes = document.querySelectorAll('.todo-checkbox');
        const total = rowCheckboxes.length;
        const checked = Array.from(rowCheckboxes).filter(cb => cb.checked).length;

        if (!header) return;

        if (checked === 0) {
            header.checked = false;
            header.indeterminate = false;
        } else if (checked === total) {
            header.checked = true;
            header.indeterminate = false;
        } else {
            header.checked = false;
            header.indeterminate = true;
        }
    }

    // --------------------------
    // 批量打开等核心功能(保持不变)
    // --------------------------
    function getTableLinks() {
        const links = [];
        const rows = document.querySelectorAll('.el-table__body tr.el-table__row');
        rows.forEach(row => {
            const cellDiv = row.querySelector('.itemTitle .item-title--click')?.closest('.cell') || row.querySelector('.cell');
            const checkbox = cellDiv && cellDiv.querySelector('.todo-checkbox');
            const link = row.querySelector('.itemTitle .link') || row.querySelector('.link');
            if (cellDiv && checkbox && checkbox.checked && link) links.push(link);
        });
        return links;
    }

    function openLinks(links) {
        if (links.length === 0) {
            alert('当前未选中任何待办任务。');
            return;
        }
        links.forEach(link => {
            const ev = new MouseEvent('click', { ctrlKey: true, bubbles: true, cancelable: true });
            link.dispatchEvent(ev);
        });
    }

    function addFixedButton() {
        if (document.getElementById('bulkOpenBtn')) return;
        const button = document.createElement('button');
        button.id = 'bulkOpenBtn';
        button.innerText = '批量打开';
        button.onclick = bulkOpenLinks;
        document.body.appendChild(button);
    }

    document.addEventListener('keydown', event => {
        if (event.ctrlKey && event.shiftKey && event.key === 'O') {
            event.preventDefault();
            bulkOpenLinks();
        }
    });

    async function bulkOpenLinks() {
        try {
            await waitForElement('.el-table__body', 5000);
            addCheckboxes();
            addHeaderCheckbox();
            const links = getTableLinks();
            openLinks(links);

            // 批量打开后恢复全选
            const rowCheckboxes = document.querySelectorAll('.todo-checkbox');
            rowCheckboxes.forEach(cb => cb.checked = true);
            const header = document.querySelector('.header-checkbox');
            if (header) { header.checked = true; header.indeterminate = false; }

        } catch (error) {
            console.error('打开失败:', error);
            alert('加载表格失败,请稍后重试。');
        }
    }

    // --------------------------
    // 监听表格及标签变化(覆盖 SPA 渲染情况)
    // --------------------------
    function observeTableAndTabs() {
        // 优先观察表格容器的父节点(如果存在),否则回退到 body
        const mainContainer = document.querySelector('.el-table__body')?.parentElement || document.body;
        const obs = new MutationObserver(() => {
            addCheckboxes();
            addHeaderCheckbox();
            highlightTodoTabs();
        });
        obs.observe(mainContainer, { childList: true, subtree: true, characterData: true });
    }

    // 初始化
    addFixedButton();
    waitForElement('.el-menu--horizontal', 5000).then(observeTableAndTabs);

})();