卡世界数据导出器

卡世界功能拓展

目前为 2025-02-02 提交的版本。查看 最新版本

// ==UserScript==
// @name        卡世界数据导出器
// @namespace   http://tampermonkey.net/
// @license     Apache-2.0
// @version     0.4
// @author      byhgz
// @description 卡世界功能拓展
// @icon        data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @noframes    
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @grant       GM_addStyle
// @grant       GM_registerMenuCommand
// @match       https://ksjhaoka.com/*
// @require     https://cdn.jsdelivr.net/npm/vue@2
// @require     https://unpkg.com/element-ui/lib/index.js
// ==/UserScript==
"use strict";
(function (Vue) {
    'use strict';
    var defCss = `
body {
    overflow: hidden;
}
.el-vertical-center {
    display: flex;
    justify-content: center;
}
.el-horizontal-center {
    display: flex;
    align-items: center;
}
.el-horizontal-right {
    display: flex;
    justify-content: flex-end;
}
.el-horizontal-left {
    display: flex;
    justify-content: flex-start;
}
`;
    class EventEmitter {
        #regularEvents = {
            events: {},
            futures: {}
        }
        #callbackEvents = {
            events: {},
            callbackInterval: 1500
        }
        on(eventName, callback) {
            const events = this.#regularEvents.events;
            if (events[eventName]) {
                events[eventName].push(callback);
                return
            }
            events[eventName] = [];
            events[eventName].push(callback);
            const futureEvents = this.#regularEvents.futures;
            if (futureEvents[eventName]) {
                for (const futureEvent of futureEvents[eventName]) {
                    callback(...futureEvent);
                }
                delete futureEvents[eventName];
            }
        }
        once(eventName, callback) {
            const onceCallback = (...args) => {
                callback(...args);
                this.#removeCallback(eventName, onceCallback);
            };
            this.on(eventName, onceCallback);
        }
        handler(eventName, callback) {
            const handlerEvents = this.#callbackEvents.events;
            if (handlerEvents[eventName]) {
                throw new Error('该事件名已经存在,请更换事件名')
            }
            handlerEvents[eventName] = callback;
        }
        invoke(eventName, ...data) {
            return new Promise(resolve => {
                const handlerEvents = this.#callbackEvents.events;
                if (handlerEvents[eventName]) {
                    resolve(handlerEvents[eventName](...data));
                    return
                }
                const i1 = setInterval(() => {
                    if (handlerEvents[eventName]) {
                        clearInterval(i1);
                        resolve(handlerEvents[eventName](...data));
                    }
                }, this.#callbackEvents.callbackInterval);
            })
        }
        send(eventName, ...data) {
            const ordinaryEvents = this.#regularEvents;
            const events = ordinaryEvents.events;
            const event = events[eventName];
            if (event) {
                for (const callback of event) {
                    callback(...data);
                }
                return;
            }
            const futures = ordinaryEvents.futures;
            if (futures[eventName]) {
                futures[eventName].push(data);
                return;
            }
            futures[eventName] = [];
            futures[eventName].push(data);
        }
        #removeCallback(eventName, callback) {
            const events = this.#regularEvents.events;
            if (events[eventName]) {
                events[eventName] = events[eventName].filter(cb => cb !== callback);
            }
            const handlerEvents = this.#callbackEvents.events;
            if (handlerEvents[eventName]) {
                handlerEvents[eventName] = handlerEvents[eventName].filter(cb => cb !== callback);
            }
        }
        off(eventName) {
            const events = this.#regularEvents.events;
            if (events[eventName]) {
                delete events[eventName];
                return true
            }
            const handlerEvents = this.#callbackEvents.events;
            if (handlerEvents[eventName]) {
                delete handlerEvents[eventName];
                return true
            }
            return false
        }
        setInvokeInterval(interval) {
            this.#callbackEvents.callbackInterval = interval;
        }
        getEvents() {
            return {
                regularEvents: this.#regularEvents,
                callbackEvents: this.#callbackEvents
            }
        }
    }
    const eventEmitter = new EventEmitter();
    const look_content_dialog_vue = {
        template: `
      <div>
      <el-dialog
          :fullscreen="true"
          title="提示"
          :visible.sync="dialogVisible"
          width="30%"
          :before-close="handleClose">
        <el-input autosize
                  type="textarea"
                  v-model="content"></el-input>
        <span slot="footer" class="dialog-footer">
    <el-button @click="dialogVisible = false">取 消</el-button>
    <el-button type="primary" @click="dialogVisible = false">确 定</el-button>
  </span>
      </el-dialog>
      </div>`,
        data() {
            return {
                dialogVisible: false,
                content: ''
            }
        },
        methods: {
            handleClose(done) {
                this.$confirm('确认关闭?')
                    .then(_ => {
                        done();
                    })
                    .catch(_ => {
                    });
            }
        },
        created() {
            eventEmitter.on('展示内容对话框', (newContent) => {
                this.content = newContent;
                this.$message('已更新内容');
                this.dialogVisible = true;
            });
        }
    };
    function findElementUntilFound(selector, config = {}) {
        const defConfig = {
            doc: document,
            interval: 1000,
            timeout: -1,
        };
        config = {...defConfig, ...config};
        return new Promise((resolve, reject) => {
            const i1 = setInterval(() => {
                const element = config.doc.querySelector(selector);
                if (element) {
                    resolve(element);
                    clearInterval(i1);
                }
            }, config.interval);
            if (config.timeout > 0) {
                setTimeout(() => {
                    clearInterval(i1);
                    reject(null); // 超时则返回 null
                }, config.timeout);
            }
        });
    }
    const findElement = async (selector, config = {}) => {
        try {
            const defConfig = {
                doc: document,
                interval: 1000,
                timeout: -1,
            };
            config = {...defConfig, ...config};
            const el = await findElementUntilFound(selector, config);
            if (config.timeout === -1) {
                return el
            }
            return {state: true, data: el}
        } catch (e) {
            return {state: false, data: e}
        }
    };
    const findElements = async (selector, config = {}) => {
        const defConfig = {doc: document, interval: 1000, timeout: -1};
        config = {...defConfig, ...config};
        try {
            const elList = await findElementsUntilFound(selector, config);
            if (config.timeout === -1) {
                return elList
            }
            return {state: true, data: elList}
        } catch (e) {
            return {state: false, data: e}
        }
    };
    function findElementsUntilFound(selector, config = {}) {
        const defConfig = {doc: document, interval: 1000, timeout: -1};
        config = {...defConfig, ...config};
        return new Promise((resolve, reject) => {
            const i1 = setInterval(() => {
                const elements = config.doc.querySelectorAll(selector);
                if (elements.length > 0) {
                    resolve(Array.from(elements));
                    clearInterval(i1);
                }
            }, config.interval);
            if (config.timeout > 0) {
                setTimeout(() => {
                    clearInterval(i1);
                    reject(null); // 超时则返回 null
                }, config.timeout);
            }
        });
    }
    var elUtil = {
        findElement,
        findElements
    };
    const isGoodsSalePage = (url = window.location.href) => {
        return url.endsWith('ksjhaoka.com/#/goods/sale');
    };
    const getPromotionCenterData = async () => {
        const elList = await elUtil.findElements('.app-main .el-row>div');
        const list = [];
        for (const el of elList) {
            const data = {};
            data["运营商"] = el.querySelector(".goods-header-title").textContent.match(/运营商:(.+)/)[1];
            data["上架时间"] = el.querySelector(".goods-header-time").textContent.match(/上架时间:(.+)/)[1];
            const originalTitle = el.querySelector(".overflow-ellipsis").textContent;
            data["卡名"] = originalTitle.substring(2, originalTitle.indexOf("卡") + 1);
            data["原始标题"] = originalTitle;
            const tempTitleInfo = originalTitle.substring(originalTitle.indexOf("卡") + 1);
            const monthlyRentIndexOf = tempTitleInfo.indexOf("元");
            let temp_monthlyRent;
            let mainParameter;//主要参数
            if (monthlyRentIndexOf !== -1) {
                temp_monthlyRent = tempTitleInfo.substring(0, monthlyRentIndexOf);
                mainParameter = tempTitleInfo.substring(monthlyRentIndexOf + 1, tempTitleInfo.length);
            } else {
                temp_monthlyRent = "未描述";
                mainParameter = "获取失败";
            }
            data["主要参数"] = mainParameter;
            data["月租"] = temp_monthlyRent;
            const tagsEl = el.querySelectorAll(".goods-tab>span");
            const placeOfBelonging = tagsEl[0].textContent;
            data["归属地"] = placeOfBelonging.includes("归属地") ? placeOfBelonging.match(/(.+)归属地/)[1] : placeOfBelonging;
            data["是否支持选号"] = mainParameter.includes("支持选号") ? "是" : "未知";
            data["是否永久套餐"] = mainParameter.includes("永久套餐") ? "是" : "未知";
            data["是否支持结转"] = mainParameter.includes("支持结转") ? "是" : "未知";
            data["是否长期套餐"] = mainParameter.includes("长期套餐") ? "是" : "未知";
            data["快递"] = tagsEl[1].textContent;
            data["合约"] = tagsEl[2].textContent;
            data["年龄要求"] = tagsEl[3].textContent;
            let tempActivationPlan;
            if (tagsEl.length === 5) {
                tempActivationPlan = tagsEl[4].textContent;
            } else {
                tempActivationPlan = "未描述";
            }
            data["激活方案"] = tempActivationPlan;
            data["佣金"] = el.querySelector(".goods-brokerage div").textContent;
            for (const li of el.querySelectorAll(".goods-info-ul>.goods-info-li")) {
                const strings = li.querySelector(".goods-info-value").textContent.split(":");
                data[strings[0]] = strings[1];
            }
            data["img"] = el.querySelector(".goods-img").src;
            for (const key in data) {
                data[key] = data[key].trim();
            }
            list.push(data);
        }
        return list;
    };
    const appendPromotionCenterData = async (list) => {
        const pageList = await getPromotionCenterData();
        const newDataList = [];
        const oldSize = list.length;
        for (let newData of pageList) {
            if (list.some(item => item["产品ID"] === newData["产品ID"])) {
                continue
            }
            newDataList.push(newData);
        }
        for (let data of newDataList) {
            list.push(data);
        }
        return oldSize < list.length
    };
    var goodsSale = {
        isGoodsSalePage,
        appendPromotionCenterData,
        getPromotionCenterData
    };
    const fileDownload = (content, fileName) => {
        const element = document.createElement('a');
        element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content));
        element.setAttribute('download', fileName);
        element.style.display = 'none';
        document.body.appendChild(element);
        element.click();
        document.body.removeChild(element);
    };
    const parseUrl = (urlString) => {
        const url = new URL(urlString);
        const pathSegments = url.pathname.split('/').filter(segment => segment !== '');
        const searchParams = new URLSearchParams(url.search.slice(1));
        const queryParams = {};
        for (const [key, value] of searchParams.entries()) {
            queryParams[key] = value;
        }
        return {
            protocol: url.protocol,
            hostname: url.hostname,
            port: url.port,
            pathname: url.pathname,
            pathSegments,
            search: url.search,
            queryParams,
            hash: url.hash
        };
    };
    var defUtil = {
        fileDownload,
        parseUrl
    };
    const promotion_center_vue = {
        template: `
      <div>
      <el-card>
        <span>数据信息</span>
        <el-badge :value="dataList.length" type="primary">
          <el-button size="small">数据</el-button>
        </el-badge>
      </el-card>
      <el-card>
        <el-button @click="getPromotionCenterDataBut">获取推广中心列表数据(当前页)</el-button>
        <el-button @click="lookDataBut">查看数据</el-button>
        <el-button type="warning" @click="clearDataBut">清空数据</el-button>
      </el-card>
      <el-card>
        <template #header>
          <span>导出</span>
        </template>
        <el-button @click='outDataBut'>导出数据(json文件)</el-button>
        <el-button @click="outDataToConsoleBut">导出数据(控制台)</el-button>
      </el-card>
      </div>`,
        data() {
            return {
                dataList: []
            }
        },
        methods: {
            clearDataBut() {
                this.dataList = [];
                this.$message('已清空数据!');
            },
            lookDataBut() {
                this.$message('获取成功!');
                eventEmitter.send('展示内容对话框', JSON.stringify(this.dataList, null, 4));
            },
            outDataToConsoleBut() {
                if (this.dataList.length === 0) {
                    this.$alert('数据为空,请先获取数据');
                    return
                }
                console.log("===========当前页面产品列表===========");
                console.log(this.dataList);
                console.log("===========当前页面产品列表===========");
                this.$alert('已导出到控制台,可通过f12查看');
            },
            outDataBut() {
                const content = JSON.stringify(this.dataList, null, 4);
                const size = this.dataList.length;
                defUtil.fileDownload(content, `卡世界产品管理-推广中心-${size}.json`);
                this.$message('已导出数据!');
            },
            async getPromotionCenterDataBut() {
                if (!goodsSale.isGoodsSalePage()) {
                    this.$message('当前页面不是推广中心页面');
                    return
                }
                const isAppendMode = await eventEmitter.invoke('isAppendMode');
                if (isAppendMode) {
                    const bool = await goodsSale.appendPromotionCenterData(this.dataList);
                    if (bool) {
                        this.$message('已追加成功!');
                    } else {
                        this.$message('追加失败,数据未变化');
                    }
                } else {
                    this.dataList = await goodsSale.getPromotionCenterData();
                }
                const isViewMode = await eventEmitter.invoke('isViewMode');
                if (isViewMode) {
                    this.lookDataBut();
                }
            }
        }
    };
    const product_management_vue = {
        template: `
      <div>
      <el-card>
        <el-switch active-text="追加模式" v-model="isAppendMode"/>
        <el-switch active-text="获取完之后查看" v-model="isViewMode"/>
      </el-card>
      <el-tabs type="border-card">
        <el-tab-pane label="推广中心">
          <promotion_center_vue/>
        </el-tab-pane>
        <el-tab-pane label="未上架"></el-tab-pane>
        <el-tab-pane label="已下架"></el-tab-pane>
      </el-tabs>
      </div>`,
        components: {
            promotion_center_vue
        },
        data() {
            return {
                isAppendMode: true,
                isViewMode: false
            }
        },
        methods: {},
        created() {
            eventEmitter.handler('isAppendMode', () => {
                return this.isAppendMode
            });
            eventEmitter.handler('isViewMode', () => {
                return this.isViewMode
            });
        }
    };
    const mainLayoutEl = document.createElement('div');
    mainLayoutEl.style.position = 'fixed';
    mainLayoutEl.style.left = '0';
    mainLayoutEl.style.top = '0';
    mainLayoutEl.style.width = '100%';
    mainLayoutEl.style.height = '100%';
    document.body.appendChild(mainLayoutEl);
    if (document.head.querySelector('#element-ui-css') === null) {
        const linkElement = document.createElement('link');
        linkElement.rel = 'stylesheet';
        linkElement.href = 'https://unpkg.com/element-ui/lib/theme-chalk/index.css';
        linkElement.id = 'element-ui-css';
        document.head.appendChild(linkElement);
        console.log('挂载element-ui样式成功');
    }
    new Vue({
        el: mainLayoutEl,
        template: `
      <div>
      <el-drawer
          title="卡世界数据获取"
          :visible.sync="drawer"
          direction="ltr"
          size="50%">
        <el-tabs type="border-card">
          <el-tab-pane label="产品管理">
            <product_management_vue/>
          </el-tab-pane>
          <el-tab-pane label="订单管理">待开发</el-tab-pane>
          <el-tab-pane label="历史通知">待开发</el-tab-pane>
        </el-tabs>
      </el-drawer>
      <look_content_dialog_vue/>
      </div>`,
        components: {
            look_content_dialog_vue,
            product_management_vue,
        },
        data() {
            return {
                drawer: true
            }
        },
        methods: {
        },
        created() {
            eventEmitter.on('主面板开关', () => {
                this.drawer = !this.drawer;
            });
        }
    });
    const styleEl = document.createElement('style');
    styleEl.innerHTML = defCss;
    document.head.appendChild(styleEl);
    const addGMMenu = (text, func, shortcutKey = null) => {
        return GM_registerMenuCommand(text, func, shortcutKey);
    };
    var gmUtil = {
        addGMMenu
    };
    const staticRoute = (url = window.location.href, title = document.title) => {
        const parseUrl = defUtil.parseUrl(url);
        console.log('静态路由:', url, parseUrl);
    };
    const dynamicRouting = () => {
        let oldUrl = window.location.href;
        setInterval(() => {
            const newUrl = window.location.href;
            if (oldUrl === newUrl) return;
            oldUrl = newUrl;
            const title = document.title;
            callback(newUrl, title);
        }, 1000);
        const callback = (url, title) => {
            const parseUrl = defUtil.parseUrl(url);
            console.log('动态路由:', parseUrl, url, title);
        };
    };
    var router = {
        staticRoute,
        dynamicRouting
    };
    window.addEventListener('load', () => {
        console.log('卡世界页面加载完成');
        router.staticRoute();
        router.dynamicRouting();
    });
    document.addEventListener('keydown', function (event) {
        if (event.key === "`") {
            eventEmitter.send('主面板开关');
        }
    });
    gmUtil.addGMMenu('主面板开关展示', () => {
        eventEmitter.send('主面板开关');
    });
    gmUtil.addGMMenu("获取当前产品页面列表", async () => {
        if (!goodsSale.isGoodsSalePage()) {
            alert("当前页面不是产品管理");
            return;
        }
        const data = goodsSale.getPromotionCenterData();
        console.log("===========当前页面产品列表===========");
        console.log(data);
        console.log("===========当前页面产品列表===========");
        alert("已打印在控制台上");
    });
})(Vue);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址