// ==UserScript==
// @name 京东商品参数对比工具
// @namespace http://tampermonkey.net/
// @version 3.0.2
// @description 该脚本可用于对比不限数量的同类型商品(如:手机、笔记本)的详细参数
// @author Yihang Wang <[email protected]>
// @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 = '3.0.2';
var itemIDs = GM_getValue("jd-price-compare-item-ids", []);
var relatedItemIDs = getRelatedItemIDs(document);
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 detailValue = dl.querySelector('dd').textContent.trim();
values[detailKey] = 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");
return imageElement.src.replace(".360buyimg.com/n5/", ".360buyimg.com/n0/");
}
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,
},
body: body,
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
console.log(response);
return response.json();
})
.then(data => {
console.log('Response:', data);
})
.catch(error => {
console.error('Error:', error);
});
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()));
}
function createList() {
let itemIdList = GM_getValue("jd-price-compare-item-ids", []);
let endpoint = `${apiServer}/api/v1/list`;
let listId = fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Jd-Compare-Version': version,
},
body: JSON.stringify(itemIdList)
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.text();
})
.then(data => {
return compareList(data);
})
.catch(error => {
console.error('Error:', error);
});
return listId;
}
function compareList(listId) {
let endpoint = `${apiServer}/api/v1/list/${listId}/compare`;
let resultUrl = fetch(endpoint, {
method: 'GET',
headers: {
'Jd-Compare-Version': version,
},
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.text();
})
.then(data => {
GM_setValue("jd-price-compare-item-ids", []);
window.open(`${apiServer}/${data}`);
})
.catch(error => {
console.error('Error:', error);
});
return resultUrl;
}
function addSingleButton() {
let compareButton = document.createElement('a');
compareButton.href = '#';
compareButton.id = 'jd-price-compare-add-single-button';
compareButton.textContent = `添加本商品`;
compareButton.style.backgroundColor = '#3498db';
compareButton.style.color = '#ffffff';
compareButton.style.padding = '3px';
compareButton.addEventListener("click", function (event) {
event.preventDefault();
appendList(getItemID(document));
updateCompareButton();
});
let cartButtton = document.querySelector('#preview > div.preview-info > div.left-btns.shieldShopInfo');
cartButtton.appendChild(compareButton);
}
function addAllButton() {
let compareButton = document.createElement('a');
compareButton.href = '#';
compareButton.id = 'jd-price-compare-add-all-button';
compareButton.textContent = `添加 ${relatedItemIDs.length} 个所有型号`;
compareButton.style.backgroundColor = '#3498db';
compareButton.style.color = '#ffffff';
compareButton.style.padding = '3px';
compareButton.addEventListener("click", function (event) {
event.preventDefault();
relatedItemIDs.forEach(function (itemID) {
appendList(itemID);
});
});
let cartButtton = document.querySelector('#preview > div.preview-info > div.left-btns.shieldShopInfo');
cartButtton.appendChild(compareButton);
}
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 compareButton() {
let compareButton = document.createElement('a');
compareButton.href = '#';
compareButton.id = 'jd-price-compare-start-button';
compareButton.style.backgroundColor = '#3498db';
compareButton.style.color = '#ffffff';
compareButton.style.padding = '3px';
compareButton.textContent = `开始对比 (${itemIDs.length})`;
compareButton.addEventListener("click", function (event) {
event.preventDefault();
createList();
});
let cartButtton = document.querySelector('#preview > div.preview-info > div.left-btns.shieldShopInfo');
cartButtton.appendChild(compareButton);
}
function main() {
addSingleButton();
if (relatedItemIDs.length > 1) {
addAllButton();
}
compareButton();
setInterval(updateCompareButton, 512);
}
window.addEventListener('load', main);
})();