购物省钱小能手

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

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==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
// ==/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: '<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 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();
})();