// ==UserScript==
// @name 购物省钱小能手
// @namespace http://tampermonkey.net/
// @version 1.0.2
// @description 京东、京东国际、淘宝、天猫查看商品历史价格(数据来源购物党)
// @author reid
// @license MIT
// @match *://*.taobao.com/*
// @match *://*.tmall.com/*
// @match *://chaoshi.detail.tmall.com/*
// @match *://*.tmall.hk/*
// @match *://*.liangxinyao.com/*
// @match *://*.jd.com/*
// @match *://*.jd.hk/*
// @exclude *://login.taobao.com/*
// @exclude *://login.tmall.com/*
// @exclude *://uland.taobao.com/*
// @exclude *://pages.tmall.com/*
// @exclude *://wq.jd.com/*
// @require https://cdn.bootcdn.net/ajax/libs/echarts/5.2.2/echarts.common.min.js
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_openInTab
// @downloadURL https://update.greasyfork.icu/scripts/438406/%E8%B4%AD%E7%89%A9%E7%9C%81%E9%92%B1%E5%B0%8F%E8%83%BD%E6%89%8B.user.js
// @updateURL https://update.greasyfork.icu/scripts/438406/%E8%B4%AD%E7%89%A9%E7%9C%81%E9%92%B1%E5%B0%8F%E8%83%BD%E6%89%8B.meta.js
// ==/UserScript==
const util = (function () {
function randomString(e) {
e = e || 32;
let t = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz1234567890",
a = t.length,
n = "";
for (let i = 0; i < e; i++) {
n += t.charAt(Math.floor(Math.random() * a));
}
return n
}
function syncRequest(option) {
return new Promise((resolve, reject) => {
option.onload = (res) => {
resolve(res);
};
option.onerror = (err) => {
reject(err);
};
GM_xmlhttpRequest(option);
});
}
function dateFormat(date, format) {
let o = {
"M+": date.getMonth() + 1,
"d+": date.getDate(),
"H+": date.getHours(),
"m+": date.getMinutes(),
"s+": date.getSeconds(),
"q+": Math.floor((date.getMonth() + 3) / 3),
"S": date.getMilliseconds()
};
if (/(y+)/.test(format)) {
format = format.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (let k in o)
if (new RegExp("(" + k + ")").test(format))
format = format.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return format;
}
function findTargetElement(ele) {
const body = window.document;
let tabContainer;
let tryTime = 0;
const maxTryTime = 30;
return new Promise((resolve, reject) => {
let interval = setInterval(() => {
tabContainer = body.querySelector(ele);
if (tabContainer) {
clearInterval(interval);
resolve(tabContainer);
}
if ((++tryTime) === maxTryTime) {
clearInterval(interval);
reject();
}
}, 1000);
});
}
return {
random: (len) => randomString(len),
req: (option) => syncRequest(option),
dateFormat: (date, format) => dateFormat(date, format),
findTargetEle: (ele) => findTargetElement(ele)
}
})();
const commodityHistoryPrice = (function () {
const _CONFIG_ = {
activeDataProvider: 'GWDang',
icon: '',
closeIcon: '',
textDesc: '历史价格',
fadeId: 'close-history-fade',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4280.141 Safari/537.36'
};
const dataProvider = (function () {
const cache = {};
class ChartsInfo {
constructor(categories, data, heighest, minimun, name, link) {
this.categories = categories;
this.data = data;
this.heighest = heighest;
this.minimun = minimun;
this.name = name;
this.link = link;
}
}
class BasicDataProvider {
constructor(name, link) {
this.name = name;
this.link = link;
}
async load() {
}
}
class GWDangDataProvider extends BasicDataProvider {
constructor() {
super('购物党', 'https://www.gwdang.com/');
this.config = {
firstQueryPath: 'https://browser.gwdang.com/brwext/dp_query_latest?union=union_gwdang&format=jsonp',
secondQueryPath: 'https://www.gwdang.com/trend/data_www?show_prom=true&v=2&get_coupon=1&dp_id='
}
this.dataCache = null;
}
/**
* 获取数据
*/
async load() {
const config = this.config;
const link = this.link;
let mockCookie = undefined;
if (this.dataCache == null) {
const fp = util.random(32);
const dfp = util.random(60);
const firstRes = await util.req({
url: `${config.firstQueryPath}&url=${encodeURIComponent(window.location)}&fp=${fp}&dfp=${dfp}`,
method: 'GET',
headers: {
'Cookie': (mockCookie = `fp=${fp};dfp=${dfp};`),
'user-agent': _CONFIG_.userAgent,
'authority': new URL(link).host
}
});
const {dp} = JSON.parse(firstRes.responseText);
const secondRes = await util.req({
url: `${config.secondQueryPath}${dp['dp_id']}`,
method: 'GET',
headers: {
'Cookie': mockCookie,
'user-agent': _CONFIG_.userAgent,
'authority': new URL(link).host,
'referer': firstRes.finalUrl
}
});
this.dataCache = JSON.parse(secondRes.responseText);
if (this.dataCache['is_ban'] !== undefined) {
alert('需要进行验证,请在打开的新窗口完成验证后再刷新本页面。');
GM_openInTab(this.dataCache['action']['to'], {active: true, insert: true, setParent: true});
}
}
return new Promise((resolve, reject) => {
resolve(this.convert(this.dataCache));
})
}
convert({series}) {
const categories = [];
const data = [];
let longestStackItem = series[0];
for (let index = 1; index < series.length; index++) {
if (longestStackItem.period < series[index].period) {
longestStackItem = series[index];
}
}
if (longestStackItem.data === undefined) {
return null;
}
for (const split of longestStackItem.data) {
categories.push(new Date(split.x * 1000));
data.push(split.y);
}
return new ChartsInfo(categories, data, longestStackItem.max, longestStackItem.min, this.name, this.link);
}
}
return {
allocateProvider: () => {
const activeProvider = _CONFIG_.activeDataProvider;
let provider = undefined;
if (cache[activeProvider] === undefined) {
provider = eval(`new ${activeProvider}DataProvider()`);
cache[activeProvider] = provider;
} else {
provider = cache[activeProvider];
}
return provider;
}
}
})();
const dataConsumer = (function () {
class BasicConsumer {
constructor() {
this.defaultCallback = (container) => {
let div = document.createElement('div');
div.style.cssText = `width: 35px;
height: 35px; padding: 7.5px;
cursor: pointer;position: fixed;
background-color: beige; border-radius: 50%;
box-shadow: 0px 0px 24px 0px rgba(138,138,138,0.49);
right: 5rem; bottom: 3rem;`;
div.title = `${_CONFIG_.textDesc}`;
div.innerHTML += `${_CONFIG_.icon}`;
div.addEventListener('click', (target) => {
this.showHistory();
});
container.parentNode.appendChild(div);
};
this.defaultChartsOption = {
title: {
text: '商品历史价格',
left: '5%',
subtextStyle: {
color: '#e23c63'
}
},
grid: {
top: '15%'
},
xAxis: {
type: 'category',
nameLocation: 'middle',
},
yAxis: {
min: (value) => {
if (value.min < 100) {
return value.min - 50;
} else if (value.min < 1000) {
return value.min - 200;
} else {
return value.min - 1000;
}
},
max: (value) => {
if (value.max < 100) {
return value.max + 50;
} else if (value.max < 1000) {
return value.max + 200;
} else {
return value.max + 1000;
}
}
},
tooltip: {
trigger: 'axis'
},
dataZoom: [{start: 30}],
series: {
type: 'line',
name: '价格',
areaStyle: {
opacity: 0.5
},
markPoint: {
data: [
{type: 'max', name: '最大值'},
{type: 'min', name: '最小值'}
]
},
markLine: {
data: [
{type: 'average', name: '平均值'}
]
}
}
};
}
/**
* 显示价格历史
*/
showHistory(customConfig) {
this.abstractFade(customConfig)
.then((config) => this.loadHistoryInfo(config));
}
/**
* 遮罩层
*/
abstractFade(customConfig) {
if (!customConfig) {
customConfig = _CONFIG_;
}
const fadeDom = document.createElement('div');
fadeDom.id = customConfig.fadeId;
fadeDom.style.cssText = `z-index: 1000000000; width: 100%; height: 100vh; background-color: rgba(0, 0, 0, 0.85); position: fixed; top: 0; left: 0;`;
const closeBtn = document.createElement('div');
closeBtn.style.cssText = 'position: absolute; top: 2rem; right: 2rem; width: 35px; height: 35px; cursor: pointer';
closeBtn.innerHTML = customConfig.closeIcon;
closeBtn.addEventListener('click', e => {
fadeDom.parentNode.removeChild(fadeDom);
});
fadeDom.appendChild(closeBtn);
const loadDiv = document.createElement('div');
loadDiv.textContent = '数据正在请求中,请等待。。。。。。';
loadDiv.style.cssText = `font-size : 14px; color: white; position: absolute; top: 30%; left: 40%;`;
fadeDom.appendChild(loadDiv);
const body = document.getElementsByTagName('body')[0];
body.appendChild(fadeDom);
return new Promise((res, rej) => res(customConfig));
}
/**
* 遮罩层中图表数据
*/
async loadHistoryInfo(config) {
const container = document.getElementById(config.fadeId);
const divContainer = document.createElement('div');
divContainer.style.cssText = `position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%); border: 0px;
border-radius: 15px; overflow-x: hidden;
background-color: #fff; overflow: hidden; text-align: center; padding: 1.5rem 0;`;
divContainer.style.width = `80%`;
divContainer.style.height = `530px`;
dataProvider.allocateProvider().load()
.then(data => {
return new Promise((resolve, reject) => {
container.appendChild(divContainer);
if (data === null) {
divContainer.textContent = "暂无历史价格数据";
resolve("暂无历史价格数据");
} else {
const charts = this.makeCharts(data, divContainer);
resolve(charts);
}
});
});
}
/**
* 制作图表
*/
makeCharts(data, container) {
const option = this.defaultChartsOption;
option.xAxis.data = data.categories.map(e => util.dateFormat(e, 'yyyy-MM-dd'));
option.series.data = data.data.map(e => e / 100);
option.title.subtext = `最高价: ¥${data.heighest / 100} 最低价¥${data.minimun / 100}`;
const myChart = echarts.init(container);
myChart.setOption(option);
return myChart;
}
}
class JdConsumer extends BasicConsumer {
render() {
util.findTargetEle('.jdm-toolbar-tabs.J-tab')
.then((container) => {
let div = document.createElement('div');
div.className = 'J-trigger jdm-toolbar-tab';
let em = document.createElement('em');
em.className = 'tab-text';
em.innerHTML = `${_CONFIG_.textDesc}`;
div.innerHTML += `${_CONFIG_.icon}`;
const icon = div.lastChild;
icon.classList.add('hps-icon');
div.appendChild(em);
GM_addStyle(`
.hps-icon {
z-index: 2;
background-color: #7a6e6e;
position: relative;
border-radius: 3px 0 0 3px;
}
.hps-icon:hover {
background-color: #c81623;
}`);
div.addEventListener('click', (target) => {
this.showHistory();
});
container.appendChild(div);
}
).catch(e => console.warn("页面没加载完成", e));
}
}
class DefaultConsumer extends BasicConsumer {
render() {
util.findTargetEle('body')
.then(this.defaultCallback);
}
}
return {
callDataConsumer: (path) => {
let mallCase = 'Default';
let matchData = {
Jd: /jd/
};
for (let pattern in matchData) {
if (matchData[pattern].test(path)) {
mallCase = pattern;
break;
}
}
const provider = eval(`new ${mallCase}Consumer`);
provider.render();
//dataProvider.allocateProvider().load();
}
}
})();
return {
start: () => {
dataConsumer.callDataConsumer(window.location);
}
}
})();
(function () {
commodityHistoryPrice.start();
})();