// ==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: `
取 消 确 定
`, 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: `
数据信息 数据 获取推广中心列表数据(当前页) 查看数据 清空数据 导出数据(json文件) 导出数据(控制台)
`, 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: `
`, 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: `
待开发 待开发
`, 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);