mooket

银河奶牛历史价格 show history market data for milkywayidle

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

  1. // ==UserScript==
  2. // @name mooket
  3. // @namespace http://tampermonkey.net/
  4. // @version 20250413.36065
  5. // @description 银河奶牛历史价格 show history market data for milkywayidle
  6. // @author IOMisaka
  7. // @match https://www.milkywayidle.com/*
  8. // @match https://test.milkywayidle.com/*
  9. // @connect mooket.qi-e.top
  10. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  11. // @grant none
  12. // @require https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js
  13. // @require https://update.greasyfork.org/scripts/532609/1570245/MWICore.js
  14. // @run-at document-start
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18.  
  19. (function () {
  20. 'use strict';
  21. //等待window.mwi.game加载完成
  22. new Promise(resolve => {
  23. const interval = setInterval(() => {
  24. if (window?.mwi?.inited && document.body) {
  25. clearInterval(interval);
  26. resolve();
  27. }
  28. }, 500);
  29. }).then(() => {
  30. window.mwi.hookCallback(window.mwi.game, "handleMessageMarketItemOrderBooksUpdated", (_, obj) => {
  31. requestItemPrice(obj.marketItemOrderBooks.itemHrid, cur_day);
  32. });
  33. window.mwi.hookCallback(window.mwi.game, "handleMessageMarketListingsUpdated", (_, obj) => {
  34. obj.endMarketListings.forEach(order => {
  35. if (order.filledQuantity == 0) return;//没有成交的订单不记录
  36. let key = order.itemHrid + "_" + order.enhancementLevel;
  37.  
  38. let tradeItem = trade_history[key] || {}
  39. if (order.isSell) {
  40. tradeItem.sell = order.price;
  41. } else {
  42. tradeItem.buy = order.price;
  43. }
  44. trade_history[key] = tradeItem;
  45. });
  46. localStorage.setItem("mooket_trade_history", JSON.stringify(trade_history));//保存挂单数据
  47. });
  48.  
  49.  
  50. let trade_history = JSON.parse(localStorage.getItem("mooket_trade_history") || "{}");
  51.  
  52. let cur_day = 1;
  53. let curHridName = null;
  54. let curShowItemName = null;
  55. let w = "500";
  56. let h = "280";
  57.  
  58. let configStr = localStorage.getItem("mooket_config");
  59. let config = configStr ? JSON.parse(configStr) : { "dayIndex": 0, "visible": true, "filter": { "bid": true, "ask": true, "mean": true } };
  60. cur_day = config.day;//读取设置
  61.  
  62. window.onresize = function () {
  63. checkSize();
  64. };
  65. function checkSize() {
  66. if (window.innerWidth < window.innerHeight) {
  67. w = "250";
  68. h = "400";
  69. } else {
  70. w = "400";
  71. h = "250";
  72. }
  73. }
  74. checkSize();
  75.  
  76. // 创建容器元素并设置样式和位置
  77. const container = document.createElement('div');
  78. container.style.border = "1px solid #ccc"; //边框样式
  79. container.style.backgroundColor = "#fff";
  80. container.style.position = "fixed";
  81. container.style.zIndex = 10000;
  82. container.style.top = `${Math.max(0, Math.min(config.y || 0, window.innerHeight - 50))}px`; //距离顶部位置
  83. container.style.left = `${Math.max(0, Math.min(config.x || 0, window.innerWidth - 50))}px`; //距离左侧位置
  84. container.style.width = `${Math.max(0, Math.min(config.w || w, window.innerWidth - 50))}px`; //容器宽度
  85. container.style.height = `${Math.max(0, Math.min(config.h || h, window.innerHeight - 50))}px`; //容器高度
  86. container.style.resize = "both";
  87. container.style.overflow = "auto";
  88. container.style.display = "flex";
  89. container.style.flexDirection = "column";
  90. container.style.flex = "1";
  91. container.style.minHeight = "33px";
  92. container.style.minWidth = "68px";
  93. container.style.cursor = "move";
  94. container.style.userSelect = "none";
  95.  
  96. let mouseDragging = false;
  97. let touchDragging = false;
  98. let offsetX, offsetY;
  99.  
  100. let resizeEndTimer = null;
  101. container.addEventListener("resize", () => {
  102. if (resizeEndTimer) clearTimeout(resizeEndTimer);
  103. resizeEndTimer = setTimeout(save_config, 1000);
  104. });
  105. container.addEventListener("mousedown", function (e) {
  106. if (mouseDragging || touchDragging) return;
  107. const rect = container.getBoundingClientRect();
  108. if (container.style.resize === "both" && (e.clientX > rect.right - 10 || e.clientY > rect.bottom - 10)) return;
  109. mouseDragging = true;
  110. offsetX = e.clientX - container.offsetLeft;
  111. offsetY = e.clientY - container.offsetTop;
  112. });
  113.  
  114. document.addEventListener("mousemove", function (e) {
  115. if (mouseDragging) {
  116. var newX = e.clientX - offsetX;
  117. var newY = e.clientY - offsetY;
  118. container.style.left = newX + "px";
  119. container.style.top = newY + "px";
  120. }
  121. });
  122.  
  123. document.addEventListener("mouseup", function () {
  124. mouseDragging = false;
  125. save_config();
  126. });
  127.  
  128. container.addEventListener("touchstart", function (e) {
  129. if (mouseDragging || touchDragging) return;
  130. const rect = container.getBoundingClientRect();
  131. let touch = e.touches[0];
  132. if (container.style.resize === "both" && (e.clientX > rect.right - 10 || e.clientY > rect.bottom - 10)) return;
  133. touchDragging = true;
  134. offsetX = touch.clientX - container.offsetLeft;
  135. offsetY = touch.clientY - container.offsetTop;
  136. });
  137.  
  138. document.addEventListener("touchmove", function (e) {
  139. if (touchDragging) {
  140. let touch = e.touches[0];
  141. var newX = touch.clientX - offsetX;
  142. var newY = touch.clientY - offsetY;
  143. container.style.left = newX + "px";
  144. container.style.top = newY + "px";
  145. }
  146. });
  147.  
  148. document.addEventListener("touchend", function () {
  149. touchDragging = false;
  150. save_config();
  151. });
  152. document.body.appendChild(container);
  153.  
  154. const ctx = document.createElement('canvas');
  155. ctx.id = "myChart";
  156. container.appendChild(ctx);
  157.  
  158.  
  159.  
  160. // 创建下拉菜单并设置样式和位置
  161. let wrapper = document.createElement('div');
  162. wrapper.style.position = 'absolute';
  163. wrapper.style.top = '5px';
  164. wrapper.style.right = '16px';
  165. wrapper.style.fontSize = '14px';
  166.  
  167. //wrapper.style.backgroundColor = '#fff';
  168. wrapper.style.flexShrink = 0;
  169. container.appendChild(wrapper);
  170.  
  171. const days = [1, 3, 7, 14, 30, 180, 360];
  172. const dayTitle = ['1天', '3天', '1周', '2周', '1月', '半年', '一年'];
  173. cur_day = days[config.dayIndex];
  174.  
  175. let select = document.createElement('select');
  176. select.style.cursor = 'pointer';
  177. select.style.verticalAlign = 'middle';
  178. select.onchange = function () {
  179. cur_day = this.value;
  180. config.dayIndex = days.indexOf(parseInt(this.value));
  181. if (curHridName) requestItemPrice(curHridName, cur_day);
  182. save_config();
  183. };
  184.  
  185. for (let i = 0; i < days.length; i++) {
  186. let option = document.createElement('option');
  187. option.value = days[i];
  188. option.text = dayTitle[i];
  189. if (i === config.dayIndex) option.selected = true;
  190. select.appendChild(option);
  191. }
  192.  
  193. wrapper.appendChild(select);
  194.  
  195. // 创建一个容器元素并设置样式和位置
  196. const leftContainer = document.createElement('div');
  197. leftContainer.style.padding = '2px'
  198. leftContainer.style.display = 'flex';
  199. leftContainer.style.flexDirection = 'row';
  200. leftContainer.style.alignItems = 'center'
  201. container.appendChild(leftContainer);
  202.  
  203. //添加一个btn隐藏canvas和wrapper
  204. let btn_close = document.createElement('input');
  205. btn_close.type = 'button';
  206. btn_close.value = '📈隐藏';
  207. btn_close.style.margin = 0;
  208. btn_close.style.cursor = 'pointer';
  209.  
  210. leftContainer.appendChild(btn_close);
  211.  
  212.  
  213. //一个固定的文本显示买入卖出历史价格
  214. let price_info = document.createElement('div');
  215.  
  216. price_info.style.fontSize = '14px';
  217. price_info.title = "我的最近买/卖价格"
  218. price_info.style.width = "max-content";
  219. price_info.style.whiteSpace = "nowrap";
  220. price_info.style.lineHeight = '25px';
  221. price_info.style.display = 'none';
  222. price_info.style.marginLeft = '5px';
  223.  
  224. let buy_price = document.createElement('span');
  225. let sell_price = document.createElement('span');
  226. price_info.appendChild(buy_price);
  227. price_info.appendChild(sell_price);
  228. buy_price.style.color = 'red';
  229. sell_price.style.color = 'green';
  230.  
  231. leftContainer.appendChild(price_info);
  232.  
  233. let lastWidth;
  234. let lastHeight;
  235. btn_close.onclick = toggle;
  236. function toggle() {
  237. if (wrapper.style.display === 'none') {
  238. wrapper.style.display = ctx.style.display = 'block';
  239. container.style.resize = "both";
  240. btn_close.value = '📈隐藏';
  241. leftContainer.style.position = 'absolute'
  242. leftContainer.style.top = '1px';
  243. leftContainer.style.left = '1px';
  244. container.style.width = lastWidth;
  245. container.style.height = lastHeight;
  246. config.visible = true;
  247. save_config();
  248. } else {
  249. lastWidth = container.style.width;
  250. lastHeight = container.style.height;
  251. wrapper.style.display = ctx.style.display = 'none';
  252. container.style.resize = "none";
  253. container.style.width = "auto";
  254. container.style.height = "auto";
  255.  
  256.  
  257. btn_close.value = '📈显示';
  258. leftContainer.style.position = 'relative'
  259. leftContainer.style.top = 0;
  260. leftContainer.style.left = 0;
  261.  
  262. config.visible = false;
  263. save_config();
  264. }
  265. };
  266.  
  267. let chart = new Chart(ctx, {
  268. type: 'line',
  269. data: {
  270. labels: [],
  271. datasets: [{
  272. label: '市场',
  273. data: [],
  274. backgroundColor: 'rgba(255,99,132,0.2)',
  275. borderColor: 'rgba(255,99,132,1)',
  276. borderWidth: 1
  277. }]
  278. },
  279. options: {
  280. onClick: save_config,
  281. responsive: true,
  282. maintainAspectRatio: false,
  283. pointRadius: 0,
  284. pointHitRadius: 20,
  285. scales: {
  286. y: {
  287. beginAtZero: false,
  288. ticks: {
  289. // 自定义刻度标签格式化
  290. callback: showNumber
  291. }
  292. }
  293. },
  294. plugins: {
  295. title: {
  296. display: true,
  297. text: "",
  298. }
  299. }
  300. }
  301. });
  302.  
  303. function requestItemPrice(itemHridName, day = 1) {
  304. curHridName = itemHridName;
  305. cur_day = day;
  306.  
  307. let itemNameEN = window.mwi.game.state.itemDetailDict[itemHridName].name;
  308.  
  309.  
  310. curShowItemName = localStorage.getItem("i18nextLng")?.startsWith("zh") ?
  311. window.mwi.lang.zh.translation.itemNames[itemHridName] : window.mwi.lang.en.translation.itemNames[itemHridName];
  312.  
  313.  
  314. let time = day * 3600 * 24;
  315. fetch("https://mooket.qi-e.top/market", {
  316. method: "POST",
  317. headers: {
  318. "Content-Type": "application/json",
  319. },
  320. body: JSON.stringify({
  321. name: itemNameEN,
  322. time: time
  323. })
  324. }).then(res => {
  325. res.json().then(data => updateChart(data, cur_day));
  326. })
  327. }
  328.  
  329. function formatTime(timestamp, range) {
  330. const date = new Date(timestamp * 1000);
  331. const pad = n => n.toString().padStart(2, '0');
  332.  
  333. // 获取各时间组件
  334. const hours = pad(date.getHours());
  335. const minutes = pad(date.getMinutes());
  336. const day = pad(date.getDate());
  337. const month = pad(date.getMonth() + 1);
  338. const shortYear = date.getFullYear().toString().slice(-2);
  339.  
  340. // 根据时间范围选择格式
  341. switch (parseInt(range)) {
  342. case 1: // 1天:只显示时间
  343. return `${hours}:${minutes}`;
  344.  
  345. case 3: // 3天:日+时段
  346. return `${hours}:${minutes}`;
  347.  
  348. case 7: // 7天:月/日 + 时段
  349. return `${day}.${hours}`;
  350. case 14: // 14天:月/日 + 时段
  351. return `${day}.${hours}`;
  352. case 30: // 30天:月/日
  353. return `${month}/${day}`;
  354.  
  355. default: // 180天:年/月
  356. return `${shortYear}/${month}`;
  357. }
  358. }
  359.  
  360. function showNumber(num) {
  361. if (isNaN(num)) return num;
  362. if (num === 0) return "0"; // 单独处理0的情况
  363.  
  364. const absNum = Math.abs(num);
  365.  
  366. //num保留一位小数
  367. if (num < 1) return num.toFixed(2);
  368.  
  369. return absNum >= 1e10 ? `${(num / 1e9).toFixed(1)}B` :
  370. absNum >= 1e7 ? `${(num / 1e6).toFixed(1)}M` :
  371. absNum >= 1e4 ? `${Math.floor(num / 1e3)}K` :
  372. `${Math.floor(num)}`;
  373. }
  374. //data={'bid':[{time:1,price:1}],'ask':[{time:1,price:1}]}
  375. function updateChart(data, day) {
  376. //过滤异常元素
  377. for (let i = data.bid.length - 1; i >= 0; i--) {
  378. if (data.bid[i].price < 0 || data.ask[i].price < 0) {
  379. data.bid.splice(i, 1);
  380. data.ask.splice(i, 1);
  381. }
  382. }
  383. //timestamp转日期时间
  384. //根据day输出不同的时间表示,<3天显示时分,<=7天显示日时,<=30天显示月日,>30天显示年月
  385.  
  386. //显示历史价格
  387. let enhancementLevel = document.querySelector(".MarketplacePanel_infoContainer__2mCnh .Item_enhancementLevel__19g-e")?.textContent.replace("+", "") || "0";
  388. let tradeName = curHridName + "_" + parseInt(enhancementLevel);
  389. if (trade_history[tradeName]) {
  390. let buy = trade_history[tradeName].buy || "无";
  391. let sell = trade_history[tradeName].sell || "无";
  392. price_info.style.display = "inline-block";
  393. let levelStr = enhancementLevel > 0 ? "(+" + enhancementLevel + ")" : "";
  394. price_info.innerHTML = `<span style="color:red">${showNumber(buy)}</span>/<span style="color:green">${showNumber(sell)}</span>${levelStr}`;
  395. container.style.minWidth = price_info.clientWidth + 70 + "px";
  396.  
  397. } else {
  398. price_info.style.display = "none";
  399. container.style.minWidth = "68px";
  400. }
  401.  
  402. let labels = data.bid.map(x => formatTime(x.time, day));
  403.  
  404. chart.data.labels = labels;
  405.  
  406. let sma = [];
  407. let sma_size = 6;
  408. let sma_window = [];
  409. for (let i = 0; i < data.bid.length; i++) {
  410. sma_window.push((data.bid[i].price + data.ask[i].price) / 2);
  411. if (sma_window.length > sma_size) sma_window.shift();
  412. sma.push(sma_window.reduce((a, b) => a + b, 0) / sma_window.length);
  413. }
  414. chart.options.plugins.title.text = curShowItemName
  415. chart.data.datasets = [
  416. {
  417. label: '买入',
  418. data: data.bid.map(x => x.price),
  419. borderColor: '#ff3300',
  420. backgroundColor: '#ff3300',
  421. borderWidth: 1.5
  422. },
  423. {
  424. label: '卖出',
  425. data: data.ask.map(x => x.price),
  426. borderColor: '#00cc00',
  427. backgroundColor: '#00cc00',
  428. borderWidth: 1.5
  429. },
  430. {
  431. label: '均线',
  432. data: sma,
  433. borderColor: '#ff9900',
  434. borderWidth: 3,
  435. tension: 0.5,
  436. fill: true
  437. }
  438. ];
  439. chart.setDatasetVisibility(0, config.filter.ask);
  440. chart.setDatasetVisibility(1, config.filter.bid);
  441. chart.setDatasetVisibility(2, config.filter.mean);
  442.  
  443. chart.update()
  444. }
  445. function save_config() {
  446.  
  447. if (chart && chart.data && chart.data.datasets && chart.data.datasets.length == 3) {
  448. config.filter.ask = chart.getDatasetMeta(0).visible;
  449. config.filter.bid = chart.getDatasetMeta(1).visible;
  450. config.filter.mean = chart.getDatasetMeta(2).visible;
  451. }
  452. config.x = Math.max(0, Math.min(container.getBoundingClientRect().x, window.innerWidth - 50));
  453. config.y = Math.max(0, Math.min(container.getBoundingClientRect().y, window.innerHeight - 50));
  454. if (container.style.width != "auto") {
  455. config.w = container.clientWidth;
  456. config.h = container.clientHeight;
  457. }
  458.  
  459. localStorage.setItem("mooket_config", JSON.stringify(config));
  460. }
  461. setInterval(() => {
  462. if (document.querySelector(".MarketplacePanel_marketplacePanel__21b7o")?.checkVisibility()) {
  463. container.style.display = "block"
  464. } else {
  465. container.style.display = "none"
  466. }
  467. }, 1000);
  468. toggle();
  469. });
  470. })();