// ==UserScript== // @name MWI Watch Market - 奶牛的市场关注监视 // @namespace http://tampermonkey.net/ // @version test0.0.11 // @description 监视下心怡物品的当前价格,还有1day和3day的数据,数据采集于MWIAPI,1day,3day数据为自己生成,没找到获取强化装备市场数据的地方,所以无法监控强化装备市场,如果有误或者有问题可以在MWIItemWatchData的仓库下给我留言 // @author lzy // @license MIT // @match https://www.milkywayidle.com/* // @grant GM_addStyle // @downloadURL https://update.greasyfork.icu/scripts/535364/MWI%20Watch%20Market%20-%20%E5%A5%B6%E7%89%9B%E7%9A%84%E5%B8%82%E5%9C%BA%E5%85%B3%E6%B3%A8%E7%9B%91%E8%A7%86.user.js // @updateURL https://update.greasyfork.icu/scripts/535364/MWI%20Watch%20Market%20-%20%E5%A5%B6%E7%89%9B%E7%9A%84%E5%B8%82%E5%9C%BA%E5%85%B3%E6%B3%A8%E7%9B%91%E8%A7%86.meta.js // ==/UserScript== (function () { "use strict"; let market; //当前的市场数据 let itemCNname; //物品的翻译数据 let db; //indexedDB用于保存一些用户的历史数据 const WatchStoreName = "watch"; const LogStoreName = "log"; const isOnline = true; //在线获取数据开关,频繁获取github容易被ban const ShowButtonId = "mkWatchButton", //显示按钮 CleanButtonId = "mkWatchCleanButton", //清除按钮 RefreshButtonId = "mkWatchRefreshButton", //刷新按钮 HideButtonId = "mkWatchHideButton", //隐藏按钮 BoxClass = "mkWatchBox", //主体盒子 HeaderClass = "mkWatchBox_Header", HeaderLabelClass = "mkWatchBox_Header_Label", //标题 HeaderActionClass = "mkWatchBox_Header_Action", //操作按钮 ActionClass = "mkWatchBox_Action", //操作按钮 BoxContainerClass = "mkWatchBox_ItemsWatchContainer", //主体容器 ItemsClass = "mkWatchBox_Items", //物品容器 ItemsAddInputId = "mkWatchBox_Items_Input_Add", //物品输入框 ItemsAddSelectId = "mkWatchBox_Items_Select_Add", //物品选择框 ItemsRemoveInputId = "mkWatchBox_Items_Input_Remove", //物品输入框 ItemsRemoveSelectId = "mkWatchBox_Items_Select_Remove", //物品选择框 ItemsAddClass = "mkWatchBox_Items_Add", //物品容器 ItemsRemoveClass = "mkWatchBox_Items_Remove", //物品容器 ItemNameClass = "mkWatchBox_Items_Name", //物品名称 ItemAskClass = "mkWatchBox_Items_Ask", //物品出售价格 ItemBidClass = "mkWatchBox_Items_Bid", //物品收购价格 ItemGroupClass = "mkWatchBox_Items_Group"; //物品收购价格 let saveedItems = []; //保存的物品列表 let data24h, data3day; //24小时和3天的历史价格数据 function init() { const p1 = getMarkets(); const p2 = initIndexedDb(); const p3 = getItemsList(); const p4 = getItemLogMarkets(); Promise.all([p1, p2, p3, p4]).then((res) => { console.info("数据获取完毕"); initHtml(); }); } async function getMarkets() { //获取当前数据 try { let res; if (isOnline) { res = await fetch( "https://raw.githubusercontent.com/holychikenz/MWIApi/main/milkyapi.json", ); //在线数据 } else { res = await fetch("./milkyapi.json"); //本地测试数据 } const data = await res.json(); market = data.market; } catch (err) { market = null; console.error("获取市场数据失败"); } return market; } async function getItemLogMarkets() { //获取处理过后的一个历史价格数据 try { if (isOnline) { let res24h = await fetch( "https://happyplum.github.io/MWIItemWatchData/1days.json", ); data24h = await res24h.json(); let res3day = await fetch( "https://happyplum.github.io/MWIItemWatchData/3days.json", ); data3day = await res3day.json(); } else { let res24h = await fetch("./node-getMWIData/dist/1days.json"); data24h = await res24h.json(); let res3day = await fetch("./node-getMWIData/dist/3days.json"); data3day = await res3day.json(); } } catch (err) {} } function initIndexedDb() { return new Promise((resolve, reject) => { const req = indexedDB.open("milk", 1); req.onerror = (err) => { console.error("不支持indexedDB?", err); reject(err); }; req.onsuccess = (event) => { db = event.target.result; db.setData = setData; db.getData = getData; db.getAllData = getAllData; resolve(db); }; req.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(WatchStoreName)) { db.createObjectStore(WatchStoreName, { autoIncrement: true }); } if (!db.objectStoreNames.contains(LogStoreName)) { db.createObjectStore(LogStoreName, { autoIncrement: true }); } }; }); } async function setData(key, value = {}, tableName = WatchStoreName) { if (!db) await initIndexedDb(); return new Promise((resolve, reject) => { try { if (!db.objectStoreNames.contains(tableName)) { reject(new Error(`Object store "${tableName}" not found`)); return; } const transaction = db.transaction(tableName, "readwrite"); const request = transaction .objectStore(tableName) .put({ key, ...value }, key); request.onsuccess = () => { resolve(); }; request.onerror = (error) => { reject(error); }; transaction.onerror = (error) => { reject(error); }; } catch (error) { reject(error); } }); } async function getData(key, tableName = WatchStoreName) { if (!db) await initIndexedDb(); return new Promise((resolve, reject) => { const getRequest = db .transaction(tableName, "readonly") .objectStore(tableName) .get(key); getRequest.onsuccess = (event) => { resolve(event.target.result); }; getRequest.onerror = (error) => { reject(error); }; }); } async function delData(key, tableName = WatchStoreName) { if (!db) await initIndexedDb(); return new Promise((resolve, reject) => { const getRequest = db .transaction(tableName, "readwrite") .objectStore(tableName) .delete(key); getRequest.onsuccess = (event) => { resolve(event.target.result); }; getRequest.onerror = (error) => { reject(error); }; }); } async function cleanData(tableName = WatchStoreName) { if (!db) await initIndexedDb(); return new Promise((resolve, reject) => { try { if (!db.objectStoreNames.contains(tableName)) { reject(new Error(`Object store "${tableName}" not found`)); return; } const transaction = db.transaction(tableName, "readwrite"); transaction.objectStore(tableName).clear(); transaction.oncomplete = () => { resolve(); }; transaction.onerror = (error) => { reject(error); }; } catch (error) { reject(error); } }); } async function getAllData(tableName = WatchStoreName) { if (!db) await initIndexedDb(); return new Promise((resolve, reject) => { const getRequest = db .transaction(tableName, "readonly") .objectStore(tableName) .getAll(); getRequest.onsuccess = (event) => { resolve(event.target.result); }; getRequest.onerror = (error) => { reject(error); }; }); } async function getItemsList() { let res; if (isOnline) { res = await fetch( "https://happyplum.github.io/MWIItemWatchData/items.json", ); } else { res = await fetch("./node-getMWIData/dist/items.json"); } const data = await res.json(); itemCNname = data; } function getCNName(key) { const itemKey = key .toLocaleLowerCase() .replace(/'/g, "") .replace(/ /g, "_"); return itemCNname[`/items/${itemKey}`]; } function createHtml() { const html = `
市场物品监测
`; return html; } async function initHtml() { if (!document.body) { //如果body不存在,可能html还没绘制完毕,延迟1秒再执行 return setTimeout(initHtml, 1000); } //插入主体 const abody = createHtml(); document.body.insertAdjacentHTML("beforeend", abody); //插入占位按钮,还没想好用什么图标,先放个按钮,用于显示框架 const aicon = `
`; document.body.insertAdjacentHTML("beforeend", aicon); //添加下拉选框 refreshItems(); //绑定框体事件 bindButtonListener(); } function bindButtonListener() { //外框体事件 const showbutton = document.getElementById(ShowButtonId); showbutton.addEventListener("click", showOrHideBox); const hidebutton = document.getElementById(HideButtonId); hidebutton.addEventListener("click", HideBox); const refreshbutton = document.getElementById(RefreshButtonId); refreshbutton.addEventListener("click", refresh); const cleanbutton = document.getElementById(CleanButtonId); cleanbutton.addEventListener("click", clean); const headerLabel = document.querySelector(`.${HeaderLabelClass}`); headerLabel.addEventListener("mousedown", moveBox); //添加删除事件 const addSearch = document.querySelector(`#${ItemsAddInputId}`); addSearch.addEventListener("input", searchAddItem); const addbutton = document.querySelector(`.${ItemsAddClass}`); addbutton.addEventListener("click", addWatchItem); const removeSearch = document.querySelector(`#${ItemsRemoveInputId}`); removeSearch.addEventListener("input", searchRemoveItem); const removebutton = document.querySelector(`.${ItemsRemoveClass}`); removebutton.addEventListener("click", removeWatchItem); } function moveBox(e) { //拖动逻辑 const box = document.querySelector(`.${BoxClass}`); let x = e.clientX; let y = e.clientY; document.onmousemove = function (e) { let nowX = e.clientX; let nowY = e.clientY; let disX = nowX - x; let disY = nowY - y; box.style.left = `${box.offsetLeft + disX}px`; box.style.top = `${box.offsetTop + disY}px`; x = nowX; y = nowY; }; document.onmouseup = function () { document.onmousemove = null; document.onmouseup = null; }; } function showOrHideBox() { const box = document.querySelector(`.${BoxClass}`); if (box.style.display === "none") { ShowBox(); } else { HideBox(); } } function ShowBox() { const box = document.querySelector(`.${BoxClass}`); box.style.display = "block"; } function HideBox() { const box = document.querySelector(`.${BoxClass}`); box.style.display = "none"; } let reing = false; async function refresh() { if (reing) { //给RefreshButtonId增加reloading的class alert("最近刷新过了,10秒后可再刷新"); return; } reing = true; const refreshbutton = document.getElementById(RefreshButtonId); refreshbutton.classList.add("reloading"); //清空容器,来增加个过度的感觉,不然感觉反馈不太明显 const container = document.querySelector(`.${BoxContainerClass}`); container.innerHTML = ""; //刷新逻辑,需要重新获取价格数据,然后重新绘制items const p1 = getMarkets(); const p2 = getItemLogMarkets(); Promise.all([p1, p2]).then((res) => { refreshItems(); console.info("刷新完毕"); setTimeout(() => { reing = false; refreshbutton.classList.remove("reloading"); }, 10 * 1000); }); } function clean() { cleanData(WatchStoreName); cleanData(LogStoreName); refreshItems(); } let addList = []; //添加的下拉列表 let removeList = []; //删除的下拉列表 async function genSelectOptions() { //根据market生成select选项,显示需要转换成中文,value为key //分为3类,添加,删除,未知3类分批,未知类型不需要添加到select中,但是需要打印用来标注 if (!market) return; saveedItems = await getAllData(); addList = []; removeList = []; Object.keys(market).forEach((key) => { let value = getCNName(key); if (!value) { console.log(`没有找到${key}的翻译`); return; } const option = { value: key, text: value }; if (saveedItems.find((item) => item.key === key)) { removeList.push(option); } else { addList.push(option); } }); } let filterAddList = []; function searchAddItem(e) { const str = e.target.value; filterAddList = addList.filter((item) => { return item.text.includes(str); }); renderAddSelectOption(); } let filterRemoveList = []; function searchRemoveItem(e) { const str = e.target.value; filterRemoveList = removeList.filter((item) => { return item.text.includes(str); }); renderRemoveSelectOption(); } function renderAddSelectOption() { //添加下拉相关 const addSelect = document.querySelector(`#${ItemsAddSelectId}`); addSelect.innerHTML = ""; const list = filterAddList.length > 0 ? filterAddList : addList; list.forEach((item) => { const option = document.createElement("option"); option.value = item.value; option.text = item.text; addSelect.add(option); }); } function renderRemoveSelectOption() { //删除下拉相关 const removeSelect = document.querySelector(`#${ItemsRemoveSelectId}`); removeSelect.innerHTML = ""; const list = filterRemoveList.length > 0 ? filterRemoveList : removeList; list.forEach((item) => { const option = document.createElement("option"); option.value = item.value; option.text = item.text; removeSelect.add(option); }); } async function addWatchItem() { //获取当前select的值 const select = document.querySelector(`#${ItemsAddSelectId}`); const key = select.value; if (!key) return; await setData(key, { index: 0, ae: -1 }); refreshItems(); //添加完毕后要刷新下 } async function removeWatchItem() { const select = document.querySelector(`#${ItemsRemoveSelectId}`); const key = select.value; if (!key) return; await delData(key); refreshItems(); //添加完毕后要刷新下 } async function refreshItems() { await genSelectOptions(); renderAddSelectOption(); renderRemoveSelectOption(); renderItems(); } async function renderItems() { //清空容器,可能和refresh阶段有点重复,放着反正也没事 const container = document.querySelector(`.${BoxContainerClass}`); container.innerHTML = ""; if (!saveedItems) saveedItems = await getAllData(); if (saveedItems.length === 0) return; //首先获取监听物品 const watchItems = saveedItems .filter((item) => item.index !== -1) .sort((a, b) => b.index - a.index); watchItems.forEach((item) => { const html = getItemHtml(item); container.insertAdjacentHTML("beforeend", html); }); } function formatNum(num) { const number = Number(num); if (number > 1000000) { return (number / 1000000).toFixed(1) + "M"; } if (number > 1000) { return (number / 1000).toFixed(1) + "K"; } return number.toFixed(0) + ""; } function getItemHtml(item) { const key = item.key; const name = getCNName(key); return createItem(key, name); } function getslope(slope) { if (slope > 0) { return `↑`; } else if (slope < 0) { return `↓`; } return `→`; } function createItem(key, name) { const m = market[key]; const oneDay = data24h[key]; const threeDay = data3day[key]; const html = `
${name}
ask:${formatNum(m.ask)}
bid:${formatNum(m.bid)}
1day ${getslope(oneDay.slope)}
avgCom:${formatNum( oneDay.avgCombined, )}
maxAsk:${formatNum( oneDay.maxAsk, )}
avgAsk:${formatNum( oneDay.avgAsk, )}
minAsk:${formatNum( oneDay.minAsk, )}
maxBid:${formatNum( oneDay.maxBid, )}
avgBid:${formatNum( oneDay.avgBid, )}
minBid:${formatNum( oneDay.minBid, )}
3day ${getslope(threeDay.slope)}
avgCom:${formatNum( threeDay.avgCombined, )}
maxAsk:${formatNum( threeDay.maxAsk, )}
avgAsk:${formatNum(threeDay.avgAsk)}
minAsk:${formatNum( threeDay.minAsk, )}
maxBid:${formatNum( threeDay.maxBid, )}
avgBid:${formatNum(threeDay.avgBid)}
minBid:${formatNum( threeDay.minBid, )}
`; return html; } function addClass() { let modelStyle = ` #mkWatchButton { position: absolute; right: 300px; top: 40px; width: 20px; height: 20px; cursor: pointer; } #mkWatchButton svg{ fill: #faa21e; width: 20px; height: 20px; } .mkWatchBox { position: absolute; z-index:1; right: 240px; top: 100px; width: 380px; min-height: 200px; padding: 10px; background: #033963; border: #74b9ff solid 1px; border-radius: 8px; color: #fff; } .mkWatchBox .mkWatchBox_Header { font-size: 18px; font-weight: bold; margin-bottom: 10px; display: flex; justify-content: flex-end; } .mkWatchBox .mkWatchBox_Header .mkWatchBox_Header_Label { flex: 1; user-select: none; cursor: move; } .mkWatchBox .mkWatchBox_Header .mkWatchBox_Header_Action { display: flex; } .mkWatchBox_ItemsWatchContainer { display: flex; min-height: 88px; flex-wrap: wrap; overflow: auto; max-height: 440px; } .mkWatchBox_ItemsWatchContainer .mkWatchBox_Items { height: 430px; width: 100px; border: #fff solid 1px; border-radius: 3px; padding: 4px; margin: 4px; font-size: 12px; } #mkWatchBox_Items_Input_Add, #mkWatchBox_Items_Input_Remove { width: 100px; } #mkWatchBox_Items_Select_Add, #mkWatchBox_Items_Select_Remove { width: 200px; margin-left: 4px; } .mkWatchBox_Items { text-align: center; } .mkWatchBox_Items_Group { border: 1px solid #74b9ff; padding: 4px; margin: 4px 0px; } #mkWatchCleanButton, #mkWatchRefreshButton, #mkWatchHideButton { cursor: pointer; margin: 0px 4px; } #mkWatchCleanButton svg, #mkWatchRefreshButton svg, #mkWatchHideButton svg { fill: #fff; width: 20px; height: 20px; } .mkWatchBox_Items_Add, .mkWatchBox_Items_Remove { cursor: pointer; } .mkWatchBox_Items_Add svg, .mkWatchBox_Items_Remove svg { width: 20px; height: 20px; fill: #fff; margin: 0px 4px; } .mkWatchBox_Action { display: flex; } .reloading#mkWatchRefreshButton svg { fill: #aaa; cursor: not-allowed; }`; try { GM_addStyle(modelStyle); } catch (err) {} } addClass(); init(); })();