// ==UserScript==
// @name 京东商品参数对比工具
// @namespace http://tampermonkey.net/
// @version 2077.0.3
// @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.3';
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 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");
// 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 compareButton = document.createElement('a');
let statusLine = document.getElementById('jd-price-compare-status-line');
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();
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(compareButton);
}
function addAllButton() {
let compareButton = document.createElement('a');
let statusLine = document.getElementById('jd-price-compare-status-line');
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) {
statusLine.textContent = `正在获取商品(ID:${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 addCompareButton() {
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 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();
setInterval(updateCompareButton, 512);
}
window.addEventListener('load', main);
})();