电商历史价格查询

看看历史价格 拒绝当小白鼠

// ==UserScript==
// @name         电商历史价格查询
// @namespace    http://shawjie.cn
// @version      1.0.11
// @description  看看历史价格 拒绝当小白鼠
// @author       ShawJie
// @match        https://item.jd.com/*
// @match        https://detail.tmall.com/item.htm*
// @match        https://item.taobao.com/item.htm*
// @match        https://detail.liangxinyao.com/item.htm*
// @match        https://*.jd.hk/*
// @grant        GM_xmlhttpRequest
// @require      https://cdn.bootcdn.net/ajax/libs/echarts/5.1.0/echarts.common.min.js
// ==/UserScript==

const __HISTORY_PRICE_CONFIG__ = {
    activeProvider: 'GouWuDang',
    mallPattern: {
        Jd: /^http[s]?:\/\/item\.jd\.com\/\d+\.html/,
        Tmall: /^http[s]:\/\/detail\.tmall\.com\/item\.htm/,
        Taobao: /^https?:\/\/item\.taobao\.com\/item\.htm/,
        AliYao: /^https?:\/\/detail\.liangxinyao.com\/item\.htm/,
        JdGlobal: /^https?:\/\/\w+\.jd.hk\/\d+\.html/
    },
    icon: '<svg t="1600605970684" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3197" ><path d="M560.64 857.6c0 24.32-17.92 25.6-39.68 25.6-21.76 0-39.68-1.28-39.68-25.6V638.72h78.08V857.6z" fill="#CBC487" p-id="3198"></path><path d="M522.24 896c-19.2 0-52.48 0-52.48-38.4V638.72c0-7.68 5.12-12.8 12.8-12.8h78.08c7.68 0 12.8 5.12 12.8 12.8V857.6c0 38.4-32 38.4-51.2 38.4z m-26.88-244.48V857.6c0 8.96 0 12.8 26.88 12.8s26.88-2.56 26.88-12.8V651.52h-53.76z" fill="#231C1C" p-id="3199"></path><path d="M714.24 304.64c-5.12-153.6-101.12-217.6-189.44-217.6h-3.84c-88.32 0-185.6 61.44-190.72 217.6v334.08h385.28s-1.28-328.96-1.28-334.08z" fill="#B8CA43" p-id="3200"></path><path d="M715.52 651.52H330.24c-3.84 0-6.4-1.28-8.96-3.84-2.56-2.56-3.84-5.12-3.84-8.96V304.64c5.12-168.96 112.64-229.12 203.52-229.12h3.84c97.28 0 197.12 70.4 202.24 229.12v334.08c0 3.84-1.28 6.4-3.84 8.96-1.28 2.56-5.12 3.84-7.68 3.84z m-372.48-25.6h359.68V304.64c-5.12-151.04-98.56-204.8-176.64-204.8h-3.84c-78.08 0-172.8 53.76-177.92 204.8-1.28 6.4-1.28 206.08-1.28 321.28z" fill="#231C1C" p-id="3201"></path><path d="M715.52 344.32v-38.4c-5.12-153.6-101.12-217.6-189.44-217.6h-3.84c-88.32 0-185.6 61.44-190.72 217.6v38.4H396.8v75.52c0 14.08 11.52 26.88 25.6 26.88s25.6-11.52 25.6-26.88v-75.52h52.48v126.72c0 14.08 11.52 26.88 25.6 26.88s25.6-11.52 25.6-26.88v-126.72h52.48v37.12c0 14.08 11.52 26.88 25.6 26.88s25.6-11.52 25.6-26.88v-37.12h60.16z" fill="#FDE8C2" p-id="3202"></path><path d="M522.24 510.72c-20.48 0-38.4-17.92-38.4-39.68v-113.92h-26.88v62.72c0 21.76-16.64 39.68-38.4 39.68-20.48 0-38.4-17.92-38.4-39.68v-62.72h-52.48c-3.84 0-6.4-1.28-8.96-3.84-2.56-2.56-3.84-5.12-3.84-8.96v-39.68C320 135.68 427.52 75.52 518.4 75.52h6.4c97.28 0 197.12 70.4 202.24 229.12v39.68c0 3.84-1.28 6.4-3.84 8.96-2.56 2.56-5.12 3.84-8.96 3.84h-52.48v24.32c0 21.76-16.64 39.68-38.4 39.68-20.48 0-38.4-17.92-38.4-39.68v-24.32h-26.88v113.92c2.56 21.76-14.08 39.68-35.84 39.68z m-76.8-179.2h52.48c7.68 0 12.8 5.12 12.8 12.8v126.72c0 7.68 5.12 14.08 12.8 14.08s12.8-6.4 12.8-14.08v-126.72c0-7.68 5.12-12.8 12.8-12.8H601.6c7.68 0 12.8 5.12 12.8 12.8v37.12c0 7.68 5.12 14.08 12.8 14.08s12.8-6.4 12.8-14.08v-37.12c0-7.68 5.12-12.8 12.8-12.8h52.48v-25.6c-5.12-151.04-98.56-204.8-177.92-204.8h-3.84c-78.08 0-172.8 53.76-177.92 204.8v25.6h52.48c7.68 0 12.8 5.12 12.8 12.8v75.52c0 7.68 5.12 14.08 12.8 14.08 6.4 0 12.8-6.4 12.8-14.08v-75.52c-3.84-7.68 1.28-12.8 8.96-12.8z" fill="#231C1C" p-id="3203"></path></svg>',
    closeIcon: '<svg t="1605027968686" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5352"><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" p-id="5353"></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" p-id="5354"></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" p-id="5355"></path></svg>',
    textDesc: '历史价格',
    fadeId: 'close-able-history-fade',
    userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
    containerHeigh: 530,
    containerSkipLen: 580
};

const util = (function(){
    function randomString(e) {
        e = e || 32;
        var 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 regexGroupFinder(str, regex, groupName) {
        const matcher = str.match(regex);
        return matcher.groups[groupName];
    }

    function dateFormat(date, format) {
        var 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 (var 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;
    }

    return {
        random: (len) => randomString(len),
        req: (option) => syncRequest(option),
        regFinder: (str, regex, groupName) => regexGroupFinder(str, regex, groupName),
        dateFormat: (date, format) => dateFormat(date, format)
    }
})();

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 GouWuDang extends BasicDataProvider {
        constructor() {
            super('购物党', 'https://www.gwdang.com/');
            this.config = {
                main: 'https://www.gwdang.com/',
                pluginMain: 'https://browser.gwdang.com/',
                firstQueryPath: '/brwext/dp_query_latest?union=union_gwdang&format=jsonp',
                secondQueryPath: 'trend/data_www?show_prom=true&v=2&get_coupon=1&dp_id=',
                analizyPattern: {
                    regex: /var dp_id = '(?<dpid>.*)';/,
                    groupName: 'dpid'
                }
            }
            this.dataCache = null;
        }

        async load() {
            const config = this.config;
            let mockCookie = undefined;

            if (this.dataCache == null) {
                const fp = util.random(32);
                const dfp = util.random(60);
                const firstRequestRes = await util.req({
                    url: `${config.pluginMain}${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(config.main).host
                    }
                });

                const {dp} = JSON.parse(firstRequestRes.responseText);
                const chartsRes = await util.req({
                    url: `${config.main}${config.secondQueryPath}${dp['dp_id']}`,
                    method: 'GET',
                    headers: {
                        'Cookie': mockCookie,
                        'user-agent': __HISTORY_PRICE_CONFIG__.userAgent,
                        'authority': new URL(config.main).host,
                        'referer': firstRequestRes.finalUrl
                    }
                });
                this.dataCache = JSON.parse(chartsRes.responseText);
                if (this.dataCache['is_ban'] != undefined) {
                    alert('需要进行验证,请在打开的新窗口完成验证后刷新本页面再试。');
                    window.open(this.dataCache['action']['to']);
                }
            }

            return new Promise((resolve, reject) => {
                resolve(this.convert(this.dataCache));
            })
        }

        convert({series}) {
            const categories = new Array();
            const data = new Array();
            let heightest = undefined;
            let minimun = undefined;

            let longestStackItem = series[0];
            for (let index = 1; index < series.length; index++) {
                if (longestStackItem.period < series[index].period) {
                    longestStackItem = series[index];
                }
            }

            for (const split of longestStackItem.data) {
                const price = split.y;
                if (heightest == undefined || heightest < price) {
                    heightest = price;
                }
                if (minimun == undefined || minimun > price) {
                    minimun = price;
                }
                categories.push(new Date(split.x * 1000));
                data.push(price);
            }

            return new ChartsInfo(
                categories, data, heightest,
                minimun, this.name, this.link
            );
        }
    }

    return {
        allocateProvider: () => {
            const activeProvider = __HISTORY_PRICE_CONFIG__.activeProvider;
            let provider = undefined;
            if (cache[activeProvider] == undefined) {
                provider = eval(`new ${activeProvider}()`);
                cache[activeProvider] = provider;
            } else {
                provider = cache[activeProvider];
            }

            return provider;
        }
    }
})();

class BasicProvider {
    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 = `${__HISTORY_PRICE_CONFIG__.textDesc}`;
            div.innerHTML += `${__HISTORY_PRICE_CONFIG__.icon}`;

            div.addEventListener('click', (target) => {
                this.apareHistory();
            });

            container.parentNode.appendChild(div);
        };
        this.defaultChartsOption = {
            title: {
                text: '商品历史价格',
                left: '5%',
                subtextStyle: {
                    color: '#e23c63'
                },
            },
            tooltip: {
                trigger: 'axis'
            },
            grid: {
                top: '15%'
            },
            xAxis: {
                type: 'category',
                nameLocation: 'middle',
            },
            yAxis: {
                min: (value) => value.min - 200,
                max: (value) => value.max + 200
            },
            dataZoom: [
                {
                    start: 60
                }
            ],
            series: {
                name: '商品历史价格',
                type: 'line',
                color: '#b8c94e',
                markLine: {
                    silent: true,
                    data: [
                        {
                            'type': 'max',
                            'color': 'rgba(226, 60, 99, 0.6)'
                        }, {
                            'type': 'min',
                            'color': 'rgba(226, 60, 99, 0.6)'
                        }
                    ]
                }
            }
        };
    }

    apareHistory(customConfig) {
        this.abstractFade(customConfig)
            .then((config) => this.loadHistoryInfo(config));
    }

    abstractRender(targetContainer) {
        const body = window.document;

        let tabContainer;
        let tryTime = 0;
        const maxTryTime = 30;

        return new Promise((resolve, reject) => {
            let interval = setInterval(() => {
                tabContainer = body.querySelector(targetContainer);
                if (tabContainer) {
                    clearInterval(interval);
                    resolve(tabContainer);
                }
                if ((++tryTime) == maxTryTime) {
                    clearInterval(interval);
                    reject();
                }
            }, 1000);
        });
    }

    abstractFade(customConfig) {
        const body = document.getElementsByTagName('body')[0];

        if (!customConfig) {
            customConfig = __HISTORY_PRICE_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);
        body.appendChild(fadeDom);

        return new Promise((res, rej) => {
            res(customConfig);
        });
    }

    loadHistoryInfo(config) {
        const container = document.getElementById(config.fadeId);
        let width = container.offsetWidth;

        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 = `${config.containerHeigh}px`;

        dataProvider.allocateProvider().load().then(data => {
            return new Promise((resolve, reject) => {
                container.appendChild(divContainer);
                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.name}${data.link} 最高价: ¥${data.heighest / 100} 最低价¥${data.minimun / 100}`;
        option.title.sublink = data.link;

        const myChart = echarts.init(container);
        myChart.setOption(option);
        return myChart;
    }
}

class JdProvider extends BasicProvider {
    render() {
        this.abstractRender('.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 = `${__HISTORY_PRICE_CONFIG__.textDesc}`;

                div.innerHTML += `${__HISTORY_PRICE_CONFIG__.icon}`;
                const icon = div.lastChild;
                icon.classList.add('hps-icon');
                div.appendChild(em);

                const customConfig = __HISTORY_PRICE_CONFIG__;
                customConfig.containerHeigh = 530;
                customConfig.containerSkipLen = 580;

                div.addEventListener('click', (target) => {
                    this.apareHistory();
                });

                const hpsStyle = document.createElement('style');
                hpsStyle.id = 'hps-style';
                hpsStyle.type = 'text/css';
                hpsStyle.innerHTML = `
                .hps-icon {
                    z-index: 2;
                    background-color: #7a6e6e;
                    position: relative;
                    border-radius: 3px 0 0 3px;
                }

                .hps-icon:hover {
                    background-color: #c81623;
                }`;

                document.head.appendChild(hpsStyle);

                container.appendChild(div);
            }
        ).catch(e => console.warn("page load not success", e));
    }
}

class TmallProvider extends BasicProvider {
    render() {
        this.abstractRender('body')
            .then(this.defaultCallback);
    }
}

class AliYaoProvider extends BasicProvider {
    render() {
        this.abstractRender('body')
            .then(this.defaultCallback);
    }
}

class TaobaoProvider extends BasicProvider {
    render() {
        this.abstractRender('body')
            .then(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 = `${__HISTORY_PRICE_CONFIG__.textDesc}`;
            div.innerHTML += `${__HISTORY_PRICE_CONFIG__.icon}`;

            const customConfig = __HISTORY_PRICE_CONFIG__;
            customConfig.containerHeigh = 530;

            div.addEventListener('click', (target) => {
                this.apareHistory(customConfig);
            });

            container.parentNode.appendChild(div);

        });
    }
}

class JdGlobalProvider extends BasicProvider {
    render() {
        this.abstractRender('.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 = `${__HISTORY_PRICE_CONFIG__.textDesc}`;

                div.innerHTML += `${__HISTORY_PRICE_CONFIG__.icon}`;
                const icon = div.lastChild;
                icon.classList.add('hps-icon');
                div.appendChild(em);

                const customConfig = __HISTORY_PRICE_CONFIG__;
                customConfig.containerHeigh = 530;
                customConfig.containerSkipLen = 580;

                div.addEventListener('click', (target) => {
                    this.apareHistory();
                });

                const hpsStyle = document.createElement('style');
                hpsStyle.id = 'hps-style';
                hpsStyle.type = 'text/css';
                hpsStyle.innerHTML = `
                .hps-icon {
                    z-index: 2;
                    background-color: #7a6e6e;
                    position: relative;
                    border-radius: 3px 0 0 3px;
                }

                .hps-icon:hover {
                    background-color: #c81623;
                }`;

                document.head.appendChild(hpsStyle);

                container.appendChild(div);
            }
        ).catch(e => console.warn("page load not success", e));
    }
}

const kiana = (function() {
    const methods = {
        initialLogic: (path) => {
            let mallCase = undefined;
            for (let pattern in __HISTORY_PRICE_CONFIG__.mallPattern) {
                if (__HISTORY_PRICE_CONFIG__.mallPattern[pattern].test(path)) {
                    mallCase = pattern;
                    break;
                }
            }
            if (mallCase == undefined) {
                return;
            }

            const provider = eval(`new ${mallCase}Provider`);
            provider.render();
        }
    }

    return {
        initial: () => {
            try {
                methods.initialLogic(window.location);
            }catch(message){
                console.warn(message);
            }
        }
    }
})();

(async function main() {
    kiana.initial();
})();