Greasy Fork

来自缓存

Greasy Fork is available in English.

购物省钱小能手

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
})();