购物省钱小能手

京东、京东国际、淘宝、天猫查看商品历史价格(数据来源购物党)

当前为 2022-01-13 提交的版本,查看 最新版本

// ==UserScript==
// @name         购物省钱小能手
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @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
// ==/UserScript==

const _CONFIG_ = {
    activeDataProvider: 'GWDang',
    icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path style="fill: #e3a7c0;" d="M50,2.125c26.441,0,47.875,21.434,47.875,47.875c0,26.441-21.434,47.875-47.875,47.875C17.857,97.875,2.125,76.441,2.125,50C2.125,23.559,23.559,2.125,50,2.125z"></path><g class="icon"><path style="fill: #7BC156;" d="M42.401,35.96l15.197,11.053v25.563H42.401V35.96z"></path><path style="fill: #92D76C;" d="M27.201,42.178l15.199-6.22v36.616c0,0-8.186,0-11.294,0c-1.785,0-3.986-1.842-3.906-3.397C27.518,63.139,27.201,42.178,27.201,42.178z"></path><path style="fill: #64A242;" d="M72.8,69.178c0.08,1.556-2.121,3.398-3.907,3.398c-3.107,0-11.294,0-11.294,0V47.013l15.199-17.271C72.8,29.741,72.483,63.139,72.8,69.178z"></path><path style="fill: #fff;" d="M42.401,33.642c1.524,0,2.763,1.237,2.763,2.764s-1.238,2.764-2.763,2.764c-1.527,0-2.764-1.237-2.764-2.764S40.875,33.642,42.401,33.642z"></path><path style="fill: #fff;" d="M57.599,42.623c1.526,0,2.763,1.237,2.763,2.763c0,1.527-1.236,2.765-2.763,2.765c-1.525,0-2.763-1.237-2.763-2.765C54.836,43.86,56.073,42.623,57.599,42.623z"></path><path style="fill: #fff;" d="M72.8,27.424c1.524,0,2.762,1.238,2.762,2.764c0,1.527-1.237,2.765-2.762,2.765c-1.527,0-2.764-1.237-2.764-2.765C70.034,28.661,71.271,27.424,72.8,27.424z"></path><path style="fill: #fff;" d="M27.201,38.479c1.525,0,2.764,1.237,2.764,2.764s-1.238,2.765-2.764,2.765c-1.526,0-2.763-1.238-2.763-2.765C24.438,39.715,25.675,38.479,27.201,38.479z"></path></g></svg>',
    closeIcon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1025 1024"><path d="M512.775 48.024c255.612 0 464.75 209.138 464.75 464.75s-209.138 464.751-464.75 464.751-464.75-209.138-464.75-464.75 209.137-464.75 464.75-464.75m0-46.476C230.826 1.55 1.549 230.826 1.549 512.775S230.826 1024 512.775 1024 1024 794.723 1024 512.775 794.723 1.549 512.775 1.549z" fill="#ffffff"></path><path d="M336.17 309.834c-6.197 0-13.943 3.098-18.59 7.745-10.845 10.845-10.845 26.336 0 37.18l354.759 354.76c4.647 4.647 12.393 7.746 18.59 7.746s13.942-3.099 18.59-7.746c10.844-10.844 10.844-26.336 0-37.18L353.21 317.579c-4.647-6.196-10.844-7.745-17.04-7.745z" fill="#ffffff"></path><path d="M689.38 309.834c-6.197 0-13.943 3.098-18.59 7.745L317.58 672.34c-10.845 10.844-10.845 26.336 0 37.18 4.647 4.647 12.393 7.746 18.59 7.746 6.196 0 13.942-3.099 18.59-7.746l354.759-354.76c10.844-10.844 10.844-26.335 0-37.18-6.197-6.196-12.393-7.745-20.14-7.745z" fill="#ffffff"></path></svg>',
    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 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 = (response) => {
                resolve(response);
            };
            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),
        findTargetElement: (ele) => findTargetElement(ele)
    }
})();

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 firstRequestRes = 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(firstRequestRes.responseText);
                const chartsRes = await util.req({
                    url: `${config.secondQueryPath}${dp['dp_id']}`,
                    method: 'GET',
                    headers: {
                        'Cookie': mockCookie,
                        'user-agent': _CONFIG_.userAgent,
                        'authority': new URL(link).host,
                        'referer': firstRequestRes.finalUrl
                    }
                });
                this.dataCache = JSON.parse(chartsRes.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.findTargetElement('.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("page load not success", e));
        }
    }

    class DefaultConsumer extends BasicConsumer {
        render() {
            util.findTargetElement('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();
        }
    }
})();

(function () {
    dataConsumer.callDataConsumer(window.location);
})();