您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
查看交易所此刻冰淇淋的融化情况
// ==UserScript== // @name 查看交易所冰淇淋融化情况 // @namespace https://github.com/gangbaRuby // @version 1.0.0 // @license AGPL-3.0 // @description 查看交易所此刻冰淇淋的融化情况 // @author Rabbit House // @match *://www.simcompanies.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=simcompanies.com // @grant GM_info // ==/UserScript== (function () { 'use strict'; const targetIds = [153, 154]; const targetRealms = [0, 1]; let GLOBAL_REALM_ID = null; // 计算剩余量 function calculateRemainingQuantity(entry, nowTime) { const decayTime = Date.parse(entry.datetimeDecayUpdated); const a = Math.abs(nowTime - decayTime); const o = Math.round(a / (1000 * 60) / 4) * 4 / 60; return Math.floor(entry.quantity * Math.pow(1 - 0.05, o)); } // 创建浮动按钮,控制浮动框显示隐藏 function createToggleButton() { if (document.getElementById("market-toggle-button")) return; const btn = document.createElement("div"); btn.id = "market-toggle-button"; btn.textContent = "查看交易所🍦融化情况"; btn.style.cssText = ` position: fixed; bottom: 20px; right: 20px; z-index: 10001; background: #222; color: white; border-radius: 4px; padding: 6px 10px; font-size: 13px;d cursor: pointer; user-select: none; opacity: 0.85; `; btn.title = "点击显示/隐藏市场订单预览"; btn.addEventListener("mouseenter", () => btn.style.opacity = "1"); btn.addEventListener("mouseleave", () => btn.style.opacity = "0.85"); // 拖动逻辑 btn.onmousedown = function (e) { e.preventDefault(); let shiftX = e.clientX - btn.getBoundingClientRect().left; let shiftY = e.clientY - btn.getBoundingClientRect().top; function moveAt(pageX, pageY) { btn.style.left = pageX - shiftX + 'px'; btn.style.top = pageY - shiftY + 'px'; btn.style.bottom = 'auto'; btn.style.right = 'auto'; } function onMouseMove(e) { moveAt(e.pageX, e.pageY); } document.addEventListener('mousemove', onMouseMove); document.onmouseup = function () { document.removeEventListener('mousemove', onMouseMove); document.onmouseup = null; }; }; btn.ondragstart = () => false; // 点击显示/隐藏窗口 btn.onclick = () => { const box = document.getElementById("market-floating-box"); if (!box) return; box.style.display = box.style.display === "none" ? "block" : "none"; }; // 设置初始位置和附加样式 btn.style.position = 'fixed'; btn.style.left = 'unset'; btn.style.top = 'unset'; btn.style.bottom = '20px'; btn.style.right = '20px'; document.body.appendChild(btn); } // 创建浮动框,支持拖拽 function createFloatingBox(hiddenInitially = false) { if (document.getElementById("market-floating-box")) return; const box = document.createElement("div"); box.id = "market-floating-box"; box.style.cssText = ` position: fixed; left: 10px; top: 50px; z-index: 9999; background: #222; color: #eee; padding: 10px; border-radius: 6px; max-height: 60vh; width: 520px; box-shadow: 0 0 12px rgba(0,0,0,0.7); overflow: hidden; user-select: none; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; font-size: 13px; display: ${hiddenInitially ? "none" : "block"}; `; // 头部拖拽条 const header = document.createElement("div"); // 创建关闭按钮 const closeBtn = document.createElement("span"); closeBtn.textContent = "✖"; closeBtn.title = "关闭窗口"; closeBtn.style.cssText = ` float: right; margin-left: 8px; color: #aaa; cursor: pointer; font-size: 14px; `; closeBtn.onmouseenter = () => closeBtn.style.color = "#fff"; closeBtn.onmouseleave = () => closeBtn.style.color = "#aaa"; closeBtn.onclick = () => { const box = document.getElementById("market-floating-box"); if (box) box.style.display = "none"; }; header.textContent = "🍦 我的🍦😭"; header.style.cssText = ` cursor: move; padding: 6px 8px; background: #111; border-radius: 4px 4px 0 0; font-weight: bold; user-select: none; `; // 内容区,含表格和统计 const content = document.createElement("div"); content.id = "market-floating-content"; content.style.cssText = ` margin-top: 8px; max-height: calc(60vh - 40px); overflow: hidden; display: flex; flex-direction: column; `; box.appendChild(header); header.appendChild(closeBtn); // 放入 header 最后 box.appendChild(content); // 添加右下角缩放控件 const resizer = document.createElement("div"); resizer.style.cssText = ` width: 12px; height: 12px; position: absolute; right: 2px; bottom: 2px; cursor: nwse-resize; background: #666; border-radius: 2px; z-index: 10000; `; box.appendChild(resizer); // 缩放逻辑 resizer.addEventListener("mousedown", function (e) { e.preventDefault(); document.body.style.userSelect = "none"; // 防止选中文字 const startX = e.clientX; const startY = e.clientY; const startWidth = box.offsetWidth; const startHeight = box.offsetHeight; function doDrag(e) { const newWidth = Math.max(300, startWidth + e.clientX - startX); const newHeight = Math.max(200, startHeight + e.clientY - startY); box.style.width = newWidth + "px"; box.style.maxHeight = "unset"; box.style.height = newHeight + "px"; } function stopDrag() { document.removeEventListener("mousemove", doDrag); document.removeEventListener("mouseup", stopDrag); document.body.style.userSelect = ""; } document.addEventListener("mousemove", doDrag); document.addEventListener("mouseup", stopDrag); }); document.body.appendChild(box); // 拖拽逻辑 dragElement(box, header); } // 拖拽函数,传入浮动元素和拖拽头部元素 function dragElement(elmnt, dragHandle) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; dragHandle.onmousedown = dragMouseDown; function dragMouseDown(e) { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; let newTop = elmnt.offsetTop - pos2; let newLeft = elmnt.offsetLeft - pos1; // 限制不出屏幕(简易) newTop = Math.max(0, Math.min(window.innerHeight - elmnt.offsetHeight, newTop)); newLeft = Math.max(0, Math.min(window.innerWidth - elmnt.offsetWidth, newLeft)); elmnt.style.top = newTop + "px"; elmnt.style.left = newLeft + "px"; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } } // 渲染表格和统计,分两块显示,表格滚动独立 function renderFloatingTable(data) { createFloatingBox(); createToggleButton(); const container = document.getElementById("market-floating-content"); if (!data || data.length === 0) { container.innerHTML = "<b>暂无数据</b>"; return; } // 表格和统计容器 container.innerHTML = ""; const tableContainer = document.createElement("div"); tableContainer.style.cssText = ` flex-grow: 1; overflow: auto; maxHeight: "calc(60vh - 80px)"; background: #333; border-radius: 4px; padding: 6px; `; const summaryContainer = document.createElement("div"); summaryContainer.style.cssText = ` margin-top: 8px; maxHeight: 100px; overflow: auto; background: #111; border-radius: 4px; padding: 6px; font-size: 12px; line-height: 1.4; color: #ccc; `; // 处理数据,加入产品名和融化数 const kindMap = { 153: "巧克力冰淇淋", 154: "苹果冰淇淋" }; const processed = data.map(row => { const melt = row.quantity - row.estimatedRemaining; return { ...row, kindName: kindMap[row.kind] || `未知(${row.kind})`, melt, }; }); // 排序参数 let sortKey = null; let sortAsc = true; // 创建表格 const table = document.createElement("table"); table.style.cssText = ` border-collapse: collapse; width: 100%; table-layout: auto; color: #eee; user-select: none; `; // 表头 const headers = [ { label: "订单ID", key: "id" }, { label: "产品名", key: "kindName" }, { label: "品质", key: "quality" }, { label: "单价", key: "price" }, { label: "数量", key: "quantity" }, { label: "剩余量", key: "estimatedRemaining" }, { label: "融化数量", key: "melt" }, { label: "公司名", key: "company" }, ]; const thead = document.createElement("thead"); const headerRow = document.createElement("tr"); for (const h of headers) { const th = document.createElement("th"); th.textContent = h.label; th.style.cssText = ` border: 1px solid #555; padding: 4px 8px; background: #111; position: sticky; top: 0; z-index: 10; cursor: pointer; user-select: none; `; th.onclick = () => { if (sortKey === h.key) { sortAsc = !sortAsc; } else { sortKey = h.key; sortAsc = true; } renderBody(); }; headerRow.appendChild(th); } thead.appendChild(headerRow); table.appendChild(thead); const tbody = document.createElement("tbody"); table.appendChild(tbody); tableContainer.appendChild(table); container.appendChild(tableContainer); container.appendChild(summaryContainer); function renderBody() { tbody.innerHTML = ""; let sorted = [...processed]; if (sortKey) { sorted.sort((a, b) => { const v1 = a[sortKey], v2 = b[sortKey]; if (typeof v1 === "number" && typeof v2 === "number") { return sortAsc ? v1 - v2 : v2 - v1; } return sortAsc ? String(v1).localeCompare(String(v2)) : String(v2).localeCompare(String(v1)); }); } for (const row of sorted) { const tr = document.createElement("tr"); for (const h of headers) { const td = document.createElement("td"); td.style.cssText = ` border: 1px solid #555; padding: 4px 8px; white-space: nowrap; max-width: 80px; overflow: hidden; text-overflow: ellipsis; color: #eee; `; if (h.key === "price") { td.textContent = row[h.key].toFixed(3); } else { td.textContent = row[h.key]; } tr.appendChild(td); } tbody.appendChild(tr); } renderSummary(); } // 统计渲染 function renderSummary() { summaryContainer.innerHTML = ""; const summaryMap = new Map(); const qualityTotalMap = new Map(); let grandTotal = 0; for (const row of processed) { if (row.melt <= 0) continue; const company = row.company; const quality = row.quality; if (!summaryMap.has(company)) summaryMap.set(company, { total: 0, q: {} }); const record = summaryMap.get(company); record.total += row.melt; record.q[quality] = (record.q[quality] || 0) + row.melt; qualityTotalMap.set(quality, (qualityTotalMap.get(quality) || 0) + row.melt); grandTotal += row.melt; } const lines = []; if (grandTotal > 0) { const sortedQualities = Array.from(qualityTotalMap.entries()) .sort(([a], [b]) => a - b) .map(([q, amt]) => `Q${q}=${amt}`); lines.push(`<div>🏪 当前交易所融化:${sortedQualities.join(",")}(共 ${grandTotal})</div>`); } if (summaryMap.size > 0) { lines.push(`<div>🧾 公司融化统计:</div>`); const sortedCompanies = Array.from(summaryMap.entries()) .sort(([, a], [, b]) => b.total - a.total); for (const [company, info] of sortedCompanies) { const parts = Object.entries(info.q) .sort(([a], [b]) => a - b) .map(([q, amt]) => `Q${q}=${amt}`) .join(","); lines.push(`<div>${company}:${parts}(共 ${info.total})</div>`); } } summaryContainer.innerHTML = lines.join(""); } // 首次渲染 renderBody(); } // 拦截 fetch const originalFetch = window.fetch; window.fetch = async function (...args) { const url = args[0]; const match = typeof url === 'string' && url.match(/\/api\/v3\/market\/(\d+)\/(\d+)\/$/); if (match) { const realm = parseInt(match[1]), id = parseInt(match[2]); if (targetRealms.includes(realm) && targetIds.includes(id)) { const response = await originalFetch(...args); response.clone().json().then(json => { processMarketData(json, realm, id); }); return response; } } return originalFetch(...args); }; // 拦截 XMLHttpRequest const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, ...rest) { this._isTargetMarketRequest = false; const match = typeof url === 'string' && url.match(/\/api\/v3\/market\/(\d+)\/(\d+)\/$/); if (match) { const realm = parseInt(match[1], 10); const id = parseInt(match[2], 10); if (targetRealms.includes(realm) && targetIds.includes(id)) { this._isTargetMarketRequest = true; this._realm = realm; this._id = id; } } return originalOpen.call(this, method, url, ...rest); }; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function (...args) { if (this._isTargetMarketRequest) { this.addEventListener("load", () => { try { const json = JSON.parse(this.responseText); processMarketData(json, this._realm, this._id); } catch (e) { console.error("JSON parse failed:", e); } }); } return originalSend.call(this, ...args); }; // 监听 DOM 变化,提取 realmId const domObserver = new MutationObserver(() => { const realmId = getRealmIdFromLink(); if (realmId !== null && realmId !== GLOBAL_REALM_ID) { console.log('[RegionAutoUpdater] 获取到 realmId:', realmId); GLOBAL_REALM_ID = realmId; tryAutoRenderFromStorage(); } }); domObserver.observe(document.body, { childList: true, subtree: true }); // 监听 URL 变化 function addUrlChangeListener(callback) { let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; callback(url); } }).observe(document, { subtree: true, childList: true }); } addUrlChangeListener(url => { tryAutoRenderFromStorage(); }); // 获取 realmId 函数 function getRealmIdFromLink() { const link = document.querySelector('a[href*="/company/"]'); if (link) { const match = link.href.match(/\/company\/(\d+)\//); return match ? parseInt(match[1], 10) : null; } return null; } // 从 localStorage 读取数据渲染 function tryAutoRenderFromStorage() { const match = location.pathname.match(/^\/zh-cn\/market\/resource\/(\d+)\/?$/); if (!match) return; const id = parseInt(match[1]); if (!targetIds.includes(id)) return; const realm = GLOBAL_REALM_ID !== null ? GLOBAL_REALM_ID : 0; const dataStr = localStorage.getItem(`marketFloating_${realm}_${id}`); if (dataStr) { try { const data = JSON.parse(dataStr); renderFloatingTable(data); } catch (e) { console.error("localStorage 数据解析失败", e); } } else { const container = document.getElementById("market-floating-content"); if (container) container.innerHTML = "<b>暂无数据</b>"; } } // 处理数据存储并渲染 function processMarketData(json, realm, id) { const now = Date.now(); const processed = json.map(entry => ({ id: entry.id, kind: entry.kind, quality: entry.quality, price: entry.price, quantity: entry.quantity, estimatedRemaining: calculateRemainingQuantity(entry, now), datetimeDecayUpdated: entry.datetimeDecayUpdated, company: entry.seller?.company || "未知公司" })); localStorage.setItem(`marketFloating_${realm}_${id}`, JSON.stringify(processed)); renderFloatingTable(processed); } // 页面首次尝试渲染 tryAutoRenderFromStorage(); createToggleButton(); createFloatingBox(true); const localVersion = GM_info.script.version; const scriptUrl = 'https://simcompanies-scripts.pages.dev/checkIceCreamMeltInMarket.user.js?t=' + Date.now(); const downloadUrl = 'https://simcompanies-scripts.pages.dev/checkIceCreamMeltInMarket.user.js'; function compareVersions(v1, v2) { const a = v1.split('.').map(Number); const b = v2.split('.').map(Number); const len = Math.max(a.length, b.length); for (let i = 0; i < len; i++) { const n1 = a[i] || 0; const n2 = b[i] || 0; if (n1 > n2) return 1; if (n1 < n2) return -1; } return 0; } function checkUpdate() { fetch(scriptUrl) .then(r => r.text()) .then(text => { const match = text.match(/@version\s+([0-9.]+)/); if (!match) return; const remoteVersion = match[1]; if (compareVersions(remoteVersion, localVersion) > 0) { if (confirm(`查看交易所冰淇淋融化情况插件发现新版本 v${remoteVersion},是否前往更新?`)) { window.open(downloadUrl, '_blank'); } } }) .catch(err => { console.warn('检查更新失败:', err); }); } setTimeout(checkUpdate, 3000); })();