京东商品参数对比工具

该脚本可用于对比不限数量的同类型商品(如:手机、笔记本)的详细参数

// ==UserScript==
// @name         京东商品参数对比工具
// @namespace    http://tampermonkey.net/
// @version      2077.0.9
// @description  该脚本可用于对比不限数量的同类型商品(如:手机、笔记本)的详细参数
// @author       Yihang Wang <wangyihanger@gmail.com>
// @match        https://item.jd.com/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==
(function () {
    'use strict';

    var apiServer = "https://jd-compare.authu.online";
    var version = '2077.0.9';
    var itemIDs = GM_getValue("jd-price-compare-item-ids", []);
    var relatedItemIDs = getRelatedItemIDs(document);
    var userID = getUserID();

    function pollUntil(conditionFn, interval = 1000, maxAttempts = 10) {
        return new Promise((resolve, reject) => {
            let attempts = 0;
            function checkCondition() {
                if (conditionFn()) {
                    resolve();
                } else if (attempts < maxAttempts) {
                    attempts++;
                    setTimeout(checkCondition, interval);
                } else {
                    reject(new Error('Polling timed out'));
                }
            }
            checkCondition();
        });
    }

    function getItemID(doc) {
        let a = doc.querySelector('a.follow.J-follow[data-id]');
        return a ? a.getAttribute('data-id') : "unknown";
    }

    function getRelatedItemIDs(doc) {
        var dataSkuValues = [getItemID(doc)];
        if (Object.keys(pageConfig.product.colorSize).length > 0) {
            pageConfig.product.colorSize.forEach(function (item) {
                dataSkuValues.push(item.skuId.toString());
            });
        }
        let itemIdSet = new Set(dataSkuValues);
        return Array.from(itemIdSet.values());
    }

    function getPrice(doc) {
        const itemID = getItemID(doc);
        const targetSelector = `.price.J-p-${itemID}`;
        const targetNode = doc.querySelector(targetSelector);
        return parseFloat(targetNode.innerText);
    }

    function getBasicInfo(doc) {
        const basicInfoElement = doc.querySelector('#detail > div.tab-con > div:nth-child(1) > div.p-parameter');
        const basicInfo = {};

        basicInfoElement.querySelectorAll('li').forEach(dl => {
            let text = dl.textContent.trim();
            console.log(text);
            if (text.indexOf(":") < 0) {
                console.error(`invalid basic info: ${text}, colon is not present`);
                return;
            }
            let items = text.split(":");
            if (items.length != 2) {
                console.error(`invalid basic info: ${text}, incorrect number of items`);
                return;
            }
            const key = items[0]
            const value = items[1]
            basicInfo[key] = value;
        });

        return basicInfo;
    }

    function getMainInfo(doc) {
        const mainInfoElements = doc.querySelectorAll('.Ptable-item');
        const mainInfo = {};

        mainInfoElements.forEach(item => {
            const key = item.querySelector('h3').textContent.trim();
            const values = {};

            item.querySelectorAll('dl').forEach(dl => {
                const detailKey = dl.querySelector('dt').textContent.trim();
                const hintElement = dl.querySelector('dd.Ptable-tips');
                var tableHint = "";
                if (hintElement != null) {
                    const tableHint = hintElement.textContent.trim();
                }
                const detailValue = dl.querySelector('dd:not(.Ptable-tips)').textContent.trim();
                const key = detailKey;
                values[key] = detailValue;
            });

            mainInfo[key] = values;
        });

        return mainInfo;
    }

    function getPackageList(doc) {
        const packageListElement = doc.querySelector('.package-list p');
        return packageListElement.textContent.trim();
    }

    function getImageUrl(doc) {
        const imageElement = doc.querySelector("#spec-list > ul > li:nth-child(1) > img");
        // https://img13.360buyimg.com/  n5/s54x54_jfs/t1/ 216364/18/36214/152585/65ade2ffFfabd3665/a7fe2701bfcd0c0a.jpg.avif
        // https://img10.360buyimg.com/  n0/jfs/t1/        235161/18/13361/139190/65bef13aF2b8cfc2f/d4f68e1e90bd1677.jpg.avif
        let items = imageElement.src.split("/");
        items[3] = 'n0'
        items[4] = 'jfs'
        items[5] = 't1'
        return items.join("/");
    }

    function parseItemWithDocument(doc) {
        let basicInfo = getBasicInfo(doc);
        let mainInfo = getMainInfo(doc);
        let packageList = getPackageList(doc);
        let imageUrl = getImageUrl(doc);
        let itemID = getItemID(doc);
        let data = {
            "商品编号": itemID,
            "基本信息": basicInfo,
            "主体信息": mainInfo,
            "包装信息": packageList,
            "价格": 'N/A',
            "图片": imageUrl,
        };
        return data;
    }

    function parseItemByID(itemID) {
        return new Promise(function (resolve, reject) {
            let endpoint = `https://item.jd.com/${itemID}.html`;
            fetch(endpoint)
                .then(response => {
                    if (!response.ok) {
                        throw new Error(`HTTP error! Status: ${response.status}`);
                    }
                    resolve(response.text());
                })
                .catch(error => {
                    console.error('Error:', error);
                });
        });
    }

    async function parseItem(itemID) {
        if (itemID == getItemID(document)) {
            pollUntil(() => (!isNaN(getPrice(document))));
            let price = getPrice(document);
            let data = parseItemWithDocument(document);
            data["价格"] = price;
            return data;
        } else {
            let html = await parseItemByID(itemID);
            let parser = new DOMParser();
            let doc = parser.parseFromString(html, 'text/html');
            let item = parseItemWithDocument(doc);
            return item;
        }
    }

    async function appendList(itemID) {
        let endpoint = `${apiServer}/api/v1/item`;
        let item = await parseItem(itemID);
        let body = JSON.stringify(item);
        fetch(endpoint, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Jd-Compare-Version': version,
                'Jd-Compare-User-Id': userID,
            },
            body: body,
        })
            .then(response => {
                if (!response.ok) {
                    throw new Error(`HTTP error! Status: ${response.status}`);
                }
                console.log(response);
                return response.json();
            })
            .then(data => {
                let itemIdList = GM_getValue("jd-price-compare-item-ids", []);
                let itemIdSet = new Set(itemIdList)
                itemIdSet.add(itemID);
                GM_setValue("jd-price-compare-item-ids", Array.from(itemIdSet.values()));
                let statusLine = document.getElementById('jd-price-compare-status-line');
                statusLine.textContent = `已成功添加 ${itemIdSet.size} 个待对比商品`;
            })
            .catch(error => {
                console.error('Error:', error);
            });
    }

    function createList() {
        let itemIdList = GM_getValue("jd-price-compare-item-ids", []);
        let statusLine = document.getElementById('jd-price-compare-status-line');
        if (itemIdList.length == 0) {
            statusLine.textContent = `当前待对比商品列表为空,请先点击添加商品按钮。`;
            return
        }
        statusLine.textContent = `正在创建 ${itemIdList.length} 个商品的对比列表...`;
        let endpoint = `${apiServer}/api/v1/list`;
        let listId = fetch(endpoint, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Jd-Compare-Version': version,
                'Jd-Compare-User-Id': userID,
            },
            body: JSON.stringify(itemIdList)
        })
            .then(response => {
                if (!response.ok) {
                    throw new Error(`HTTP error! Status: ${response.status}`);
                }
                return response.json();
            })
            .then(data => {
                statusLine.textContent = `对比成功,正在自动打开结果页面...`;
                return compareList(data._id);
            })
            .catch(error => {
                console.error('Error:', error);
            });
        return listId;
    }

    function compareList(listId) {
        let statusLine = document.getElementById('jd-price-compare-status-line');
        let endpoint = `${apiServer}/api/v1/list/${listId}/compare`;
        let resultUrl = fetch(endpoint, {
            method: 'GET',
            headers: {
                'Jd-Compare-Version': version,
                'Jd-Compare-User-Id': userID,
            },
        })
            .then(response => {
                if (!response.ok) {
                    throw new Error(`HTTP error! Status: ${response.status}`);
                }
                return response.text();
            })
            .then(data => {
                let link = document.createElement('a');
                let url = `${apiServer}/${data}`;
                link.href = url;
                link.textContent = `列表 ID: ${listId} `; // 修正属性名,应该是textContent
                statusLine.textContent = `对比完成,正在打开商品对比结果页面。`;
                statusLine.appendChild(document.createElement("br"));
                
                // 创建并添加<p>元素
                let p = document.createElement("p");
                p.textContent = `如未打开,也可以直接点击下方链接`;
                statusLine.appendChild(p);
                statusLine.appendChild(link);
                statusLine.appendChild(document.createElement("br"));
                
                // 创建并添加文本节点
                let textNode = document.createTextNode(`您可以继续添加其他待对比商品。`);
                statusLine.appendChild(textNode);
                
                GM_setValue("jd-price-compare-item-ids", []);
                window.open(url);
            })
            .catch(error => {
                console.error('Error:', error);
            });
        return resultUrl;
    }

    function addSingleButton() {
        let button = document.createElement('a');
        let statusLine = document.getElementById('jd-price-compare-status-line');
        button.href = '#';
        button.id = 'jd-price-compare-add-single-button';
        button.textContent = `添加本商品`;
        button.style.backgroundColor = '#3498db';
        button.style.color = '#ffffff';
        button.style.padding = '3px';
        button.addEventListener("click", function (event) {
            event.preventDefault();
            let itemID = getItemID(document);
            statusLine.textContent = `正在获取商品(ID:${itemID})详信息...`;
            appendList(itemID);
            updateCompareButton();
        });
        let cartButtton = document.querySelector('#preview > div.preview-info > div.left-btns.shieldShopInfo');
        cartButtton.appendChild(button);
    }

    function addAllButton() {
        let button = document.createElement('a');
        let statusLine = document.getElementById('jd-price-compare-status-line');
        button.href = '#';
        button.id = 'jd-price-compare-add-all-button';
        button.textContent = `添加所有 ${relatedItemIDs.length} 个型号`;
        button.style.backgroundColor = '#3498db';
        button.style.color = '#ffffff';
        button.style.padding = '3px';
        button.addEventListener("click", function (event) {
            event.preventDefault();
            relatedItemIDs.forEach(function (itemID) {
                statusLine.textContent = `正在获取商品(ID:${itemID})详信息...`;
                appendList(itemID);
            });
        });
        let cartButtton = document.querySelector('#preview > div.preview-info > div.left-btns.shieldShopInfo');
        cartButtton.appendChild(button);
    }

    function updateCompareButton() {
        let element = document.getElementById('jd-price-compare-start-button');
        let itemIdList = GM_getValue("jd-price-compare-item-ids", []);
        element.textContent = `开始对比 (${itemIdList.length})`;
    }

    function addCompareButton() {
        let button = document.createElement('a');
        button.href = '#';
        button.id = 'jd-price-compare-start-button';
        button.style.backgroundColor = '#3498db';
        button.style.color = '#ffffff';
        button.style.padding = '3px';
        button.textContent = `开始对比 (${itemIDs.length})`;
        button.addEventListener("click", function (event) {
            event.preventDefault();
            createList();
        });
        let cartButtton = document.querySelector('#preview > div.preview-info > div.left-btns.shieldShopInfo');
        cartButtton.appendChild(button);
    }

    function addFeedbackButton() {
        let button = document.createElement('a');
        button.href = '#';
        button.id = 'jd-price-add-feedback-button';
        button.style.backgroundColor = '#3498db';
        button.style.color = '#ffffff';
        button.style.padding = '3px';
        button.textContent = `意见反馈`;
        button.addEventListener("click", function (event) {
            event.preventDefault();
            const url = "https://greasyfork.org/zh-CN/scripts/486915-%E4%BA%AC%E4%B8%9C%E5%95%86%E5%93%81%E5%8F%82%E6%95%B0%E5%AF%B9%E6%AF%94%E5%B7%A5%E5%85%B7/feedback";
            window.open(url);
        });
        let cartButtton = document.querySelector('#preview > div.preview-info > div.left-btns.shieldShopInfo');
        cartButtton.appendChild(button);
    }

    function getUserID() {
        let userID = GM_getValue("jd-price-compare-user-id", "");
        if (userID == "") {
            userID = Math.random().toString(36).substring(2);
            GM_setValue("jd-price-compare-user-id", userID);
        }
        return userID;
    }

    function addStatusLine() {
        let statusLine = document.createElement('p');
        statusLine.id = 'jd-price-compare-status-line';
        statusLine.style.color = '#000000';
        statusLine.style.padding = '3px';
        statusLine.textContent = `京东商品参数对比工具 v${version}`;

        let statusLineDiv = document.createElement('div');
        statusLineDiv.id = 'jd-price-compare-status-line-div';
        statusLineDiv.style.textAlign = 'center';
        statusLineDiv.appendChild(statusLine);

        let targetElement = document.querySelector('#preview > div.preview-info');
        targetElement.parentNode.insertBefore(statusLineDiv, targetElement.nextSibling);
    }

    function main() {
        addStatusLine();
        addSingleButton();
        if (relatedItemIDs.length > 1) {
            addAllButton();
        }
        addCompareButton();
        addFeedbackButton();
        setInterval(updateCompareButton, 512);
    }

    window.addEventListener('load', main);
})();