ACS Tracker

track the detailed submission status on ACS Publishing Platform.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         ACS Tracker
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  track the detailed submission status on ACS Publishing Platform.
// @author       zhangkaihua88
// @match        *://publish.acs.org/app*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=acs.org
// @grant        none
// @license      Apache-2.0
// ==/UserScript==

/**
 * 监听包含特定字符串的所有API请求并返回响应数据
 * @param {string} targetString - 要匹配的目标字符串
 * @param {number} [timeout=30000] - 超时时间(毫秒)
 * @param {boolean} [captureAll=false] - 是否捕获所有匹配请求(否则只捕获第一个)
 * @returns {Promise<Object|Object[]>} - 包含响应数据的Promise(单个或数组)
 */
function listenForApiResponse(targetString, timeout = 30000, captureAll = false) {
    return new Promise((resolve, reject) => {
        // 用于存储原始 fetch 和 XMLHttpRequest 方法
        const originalFetch = window.fetch;
        const originalXhrOpen = XMLHttpRequest.prototype.open;

        // 存储所有匹配的响应
        const matchedResponses = [];

        // 超时计时器
        const timeoutId = setTimeout(() => {
            resetInterceptors();

            if (matchedResponses.length > 0) {
                resolve(captureAll ? matchedResponses : matchedResponses[0]);
            } else {
                reject(new Error(`监听超时: ${timeout}ms 内未捕获到包含 "${targetString}" 的请求`));
            }
        }, timeout);

        // 重置拦截器函数
        function resetInterceptors() {
            window.fetch = originalFetch;
            XMLHttpRequest.prototype.open = originalXhrOpen;
        }

        // 检查 URL 是否包含目标字符串
        function containsTargetString(url) {
            return url.toString().includes(targetString);
        }

        // 处理匹配的响应
        function handleMatchedResponse(data) {
            matchedResponses.push(data);

            if (!captureAll) {
                clearTimeout(timeoutId);
                resetInterceptors();
                resolve(data);
            }
        }

        // 拦截 Fetch API
        window.fetch = async function (input, init) {
            const url = typeof input === 'string' ? input : input.url;

            try {
                const response = await originalFetch.apply(this, arguments);

                // 如果URL包含目标字符串,获取响应数据
                if (containsTargetString(url)) {
                    const responseClone = response.clone();
                    const contentType = responseClone.headers.get('content-type');

                    let responseData;
                    if (contentType && contentType.includes('application/json')) {
                        responseData = await responseClone.json();
                    } else {
                        responseData = await responseClone.text();
                    }

                    handleMatchedResponse({
                        url,
                        method: init?.method || 'GET',
                        status: response.status,
                        data: responseData
                    });
                }

                return response;
            } catch (error) {
                // 如果出错且URL包含目标字符串,记录错误
                if (containsTargetString(url)) {
                    handleMatchedResponse({
                        url,
                        method: init?.method || 'GET',
                        error: error.message
                    });
                }
                throw error;
            }
        };

        // 拦截 XMLHttpRequest
        XMLHttpRequest.prototype.open = function (method, url) {
            const xhr = this;

            // 监听状态变化
            xhr.addEventListener('readystatechange', function () {
                if (xhr.readyState === 4 && containsTargetString(url)) {
                    let responseData;
                    try {
                        responseData = JSON.parse(xhr.responseText);
                    } catch (e) {
                        responseData = xhr.responseText;
                    }

                    handleMatchedResponse({
                        url,
                        method,
                        status: xhr.status,
                        data: responseData
                    });
                }
            });

            return originalXhrOpen.apply(this, arguments);
        };
    });
}

function convertToBeijingTime(dateString) {
    // 检查是否为特殊日期
    if (dateString === "0001-01-01T00:00:00") {
        return "";
    }

    const date = new Date(dateString);

    // Convert to Beijing Time (UTC +8) using toLocaleString with options
    return date.toLocaleString("zh-CN", {
        timeZone: "Asia/Shanghai", // Specify Beijing timezone
        hour12: false // Use 24-hour format
    });
}



function createTable(data) {
    // Create the table
    const table = document.createElement('table');

    // Apply basic styles
    table.style.width = '100%';
    table.style.borderCollapse = 'collapse';
    table.style.margin = '20px 0';
    table.style.fontFamily = 'Arial, sans-serif';
    table.style.boxShadow = '0 2px 12px rgba(0, 0, 0, 0.1)';

    // Create table header
    const header = table.createTHead();
    const headerRow = header.insertRow(0);
    const headers = ["Task ID", "Task Name", "Task Status Name", "Status", "Started", "Completed", "Due"];

    // Apply header styles
    headers.forEach((headerText) => {
        const th = document.createElement('th');
        th.textContent = headerText;
        th.style.backgroundColor = '#4CAF50';
        th.style.color = 'white';
        th.style.padding = '10px';
        th.style.textAlign = 'left';
        th.style.fontSize = '16px';
        headerRow.appendChild(th);
    });

    // Create table body
    const body = table.createTBody();
    data.forEach(task => {
        const row = body.insertRow();

        // Apply alternating row colors
        if (body.rows.length % 2 === 0) {
            row.style.backgroundColor = '#f9f9f9'; // even row
        }

        row.addEventListener('mouseenter', () => {
            row.style.backgroundColor = '#f5f5f5'; // hover effect
        });
        row.addEventListener('mouseleave', () => {
            row.style.backgroundColor = body.rows.length % 2 === 0 ? '#f9f9f9' : ''; // revert back to alternate color
        });

        // Insert the task data into the table
        row.insertCell(0).textContent = task.taskId;
        row.insertCell(1).textContent = task.taskName;
        row.insertCell(2).textContent = task.taskStatusName;
        row.insertCell(3).textContent = task.taskStatus;
        row.insertCell(4).textContent = convertToBeijingTime(task.datetimeStarted);
        row.insertCell(5).textContent = convertToBeijingTime(task.datetimeCompleted);
        row.insertCell(6).textContent = convertToBeijingTime(task.datetimeDue);

        // Apply cell styles
        Array.from(row.cells).forEach(cell => {
            cell.style.padding = '8px';
            cell.style.textAlign = 'left';
            cell.style.borderBottom = '1px solid #ddd';
            cell.style.fontSize = '14px';
        });
    });

    return table;
}



(function () {
    
    function startListening() {
        listenForApiResponse('integration/s1/submissions/submissionInfo?')
            .then(result => {
                console.log('匹配的API响应:', result.data.response.result.submissionStatus.task);
                // Attach the table to the page
                const taskTableContainer = document.querySelector('.page-section__container');
                const container = document.createElement('div');
                container.style.overflowX = 'auto';
                container.style.padding = '15px';
                container.appendChild(createTable(result.data.response.result.submissionStatus.task));

                // Insert the container as the first child of taskTableContainer
                const firstChild = taskTableContainer.firstChild;
                taskTableContainer.insertBefore(container, firstChild);

                // 监听结束后重新启动
                startListening();
            })
            .catch(error => {
                console.error('监听错误:', error.message);
                // 出错后也尝试重新启动
                startListening();
            });
    }

    startListening();

})();