Greasy Fork

Greasy Fork is available in English.

MWI Watch Market - 奶牛的市场关注监视

监视下心怡物品的当前价格,还有1day和3day的数据,数据采集于MWIAPI,1day,3day数据为自己生成,如果有误或者有问题可以在MWIItemWatchData的仓库下给我留言

当前为 2025-05-08 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name             MWI Watch Market - 奶牛的市场关注监视
// @namespace        http://tampermonkey.net/
// @version          test0.0.2
// @description      监视下心怡物品的当前价格,还有1day和3day的数据,数据采集于MWIAPI,1day,3day数据为自己生成,如果有误或者有问题可以在MWIItemWatchData的仓库下给我留言
// @author           lzy
// @license          MIT
// @match            https://www.milkywayidle.com/*
// @grant            GM_addStyle
// ==/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",
    HeaderLabelClas = "mkWatchBox_Header_Label", //标题
    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) => {
      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() {
    const res = await fetch("./items.json");
    const data = await res.json();
    itemCNname = data;
  }
  function createHtml() {
    const html = `
    <div class="${BoxClass}">
      <div class="${HeaderClass}">
        <div class="${HeaderLabelClas}">
          市场物品监测
        </div>
        <div>
          <button id="${CleanButtonId}">清空</button>
          <button id="${RefreshButtonId}">刷新</button>
          <button id="${HideButtonId}">隐藏</button>
        </div>
      </div>
      <div class="${BoxContainerClass}">
      </div>
       <div class="${ActionClass}">
          <input id="${ItemsAddInputId}" placeholder="物品名称筛选" autocomplete="off" /><select id="${ItemsAddSelectId}">
          </select>
          <button class="${ItemsAddClass}">添加</button>
        </div>
        <div class="${ActionClass}">
          <input id="${ItemsRemoveInputId}" placeholder="物品名称筛选" autocomplete="off" /><select id="${ItemsRemoveSelectId}">
          </select>
          <button class="${ItemsRemoveClass}">删除</button>
        </div>
    </div>`;
    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 = `<button id="${ShowButtonId}">按钮</button>`;
    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 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 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";
  }
  function refresh() {
    //刷新逻辑,需要重新获取价格数据,然后重新绘制items
    getMarkets();
    refreshItems();
  }
  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) => {
      const itemKey = key.toLocaleLowerCase().replace(/ /g, "_");
      let value = itemCNname[`/items/${itemKey}`];
      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() {
    //清空容器
    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 itemKey = key.toLocaleLowerCase().replace(/ /g, "_");
    const name = itemCNname[`/items/${itemKey}`];
    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 = `
        <div class="${ItemsClass}">
          <div class="${ItemNameClass}">${name}</div>
          <div class="${ItemGroupClass}">
            <div class="${ItemAskClass}">ask:${formatNum(m.ask)}</div>
            <div class="${ItemBidClass}">bid:${formatNum(m.bid)}</div>
          </div>
          <div class="${ItemBidClass}">1day ${getslope(oneDay.slope)}</div>
          <div class="${ItemBidClass}">avgCom:${formatNum(
      oneDay.avgCombined,
    )}</div>
          <div class="${ItemGroupClass}">
            <div class="${ItemBidClass}">maxAsk:${formatNum(
      oneDay.maxAsk,
    )}</div>
            <div class="${ItemBidClass}">avgAsk:${formatNum(
      oneDay.avgAsk,
    )}</div>
            <div class="${ItemBidClass}">minAsk:${formatNum(
      oneDay.minAsk,
    )}</div>
          </div>
          <div class="${ItemGroupClass}">
            <div class="${ItemBidClass}">maxBid:${formatNum(
      oneDay.maxBid,
    )}</div>
            <div class="${ItemBidClass}">avgBid:${formatNum(
      oneDay.avgBid,
    )}</div>
            <div class="${ItemBidClass}">minBid:${formatNum(
      oneDay.minBid,
    )}</div>
          </div>
          <div class="${ItemBidClass}">3day ${getslope(threeDay.slope)}</div>
          <div class="${ItemBidClass}">avgCom:${formatNum(
      threeDay.avgCombined,
    )}</div>
          <div class="${ItemGroupClass}">
          <div class="${ItemBidClass}">maxAsk:${formatNum(
      threeDay.maxAsk,
    )}</div>
    <div class="${ItemBidClass}">avgAsk:${formatNum(threeDay.avgAsk)}</div>
          <div class="${ItemBidClass}">minAsk:${formatNum(
      threeDay.minAsk,
    )}</div>
    </div>
    <div class="${ItemGroupClass}">
          <div class="${ItemBidClass}">maxBid:${formatNum(
      threeDay.maxBid,
    )}</div>
    <div class="${ItemBidClass}">avgBid:${formatNum(threeDay.avgBid)}</div>
          <div class="${ItemBidClass}">minBid:${formatNum(
      threeDay.minBid,
    )}</div>
    </div>
        </div>`;
    return html;
  }
  function addClass() {
    let modelStyle = `
    #mkWatchButton {
      position: absolute;
      right: 240px;
      top: 40px;
    }
    .mkWatchBox {
      position: absolute;
      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;
      cursor: pointer;
    }
    .mkWatchBox_ItemsWatchContainer {
      display: flex;
      min-height: 88px;
      flex-wrap: wrap;
      overflow: auto;
      max-height: 420px;
    }
    .mkWatchBox_ItemsWatchContainer .mkWatchBox_Items {
      height: 400px;
      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;
    }`;
    try {
      GM_addStyle(modelStyle);
    } catch (err) {}
  }
  addClass();
  init();
})();