您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
记录每日总资产增长,图表中分别显示总资产、流动资产、非流动资产详情,数据存储在本地,可查看3、7、30、60、90、180天记录
// ==UserScript== // @name 每日总资产增长(DailyAssets) // @namespace http://tampermonkey.net/ // @version 0.0.3 // @description 记录每日总资产增长,图表中分别显示总资产、流动资产、非流动资产详情,数据存储在本地,可查看3、7、30、60、90、180天记录 // @author Vicky718 // @match https://www.milkywayidle.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @license MIT // ==/UserScript== (function () { 'use strict'; // 添加样式 GM_addStyle(` #deltaNetworthChartModal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 850px; max-width: 90vw; background: #1e1e1e; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.6); z-index: 9999; display: none; flex-direction: column; } #deltaNetworthChartModal.dragging { cursor: grabbing; } #deltaNetworthChartHeader { padding: 10px 15px; background: #333; color: white; font-weight: bold; display: flex; justify-content: space-between; align-items: center; cursor: default; user-select: none; border-top-left-radius: 8px; border-top-right-radius: 8px; } #netWorthChartBody { padding: 15px; } #netWorthChart { width: 100%; height: 350px; } .asset-delta-display { text-align: left; color: #fff; font-size: 16px; margin: 0px 0; } .asset-delta-label { font-weight: bold; margin-right: 5px; } #showHistoryIcon { cursor: pointer; margin-left: 8px; font-size: 16px; display: inline-block; margin-top: 0px; } #chartOptionsContainer { padding: 10px; background: #252525; border-bottom: 1px; solid #333; } #chartDisplayOptions { display: none; /* 只隐藏显示选项部分 */ } .chart-option { margin: 5px; display: inline-block; } .chart-option input { margin-right: 5px; } .chart-option label { cursor: pointer; } .positive-delta { color: #4CAF50; font-weight: bold; } .negative-delta { color: #F44336; font-weight: bold; } .neutral-delta { color: #9E9E9E; font-weight: bold; } .time-range-btn { padding: 5px 10px; background: #444; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 5px; } .time-range-btn:hover { background: #555; } .time-range-btn.active { background: #666; font-weight: bold; } #timeRangeOptions { margin-top: 8px; color: #fff; } `); // 工具函数:将带单位的字符串转为数字 function parseFormattedNumber(str) { if (!str) return 0; const cleanStr = str.replace(/[^\d.,-]/g, '').replace(',', '.'); const num = parseFloat(cleanStr); if (isNaN(num)) return 0; if (str.includes('B') || str.includes('b')) return num * 1e9; if (str.includes('M') || str.includes('m')) return num * 1e6; if (str.includes('K') || str.includes('k')) return num * 1e3; return num; } // 工具函数:将大数字格式化为带单位的字符串 function formatLargeNumber(num) { const abs = Math.abs(num); let formatted; /* if (abs >= 1e9) { formatted = (num / 1e9).toFixed(2) + 'B'; } else */ if (abs >= 1e6) { formatted = (num / 1e6).toFixed(2) + 'M'; } else if (abs >= 1e3) { formatted = (num / 1e3).toFixed(2) + 'K'; } else { formatted = num.toFixed(2); } return formatted; } // 获取或初始化图表显示选项 function getChartOptions() { const defaults = { showCurrent: true, showNonCurrent: true, showTotal: true, daysToShow: 30 }; const saved = GM_getValue('chartOptions', defaults); return {...defaults, ...saved}; } // 保存图表显示选项 function saveChartOptions(options) { GM_setValue('chartOptions', options); } window.kbd_calculateTotalNetworth = function kbd_calculateTotalNetworth(currentAssets, nonCurrentAssets, dom) { class AssetDataStore { constructor(storageKey = 'kbd_asset_data_v2', maxDays = 180, currentRole = 'default') { this.storageKey = storageKey; this.maxDays = maxDays; this.currentRole = currentRole; this.data = this.loadFromStorage(); } setRole(roleId) { this.currentRole = roleId; } getRoleData() { if (!this.data[this.currentRole]) { this.data[this.currentRole] = {}; } return this.data[this.currentRole]; } getTodayKey() { const now = new Date(); const utcPlus8 = new Date(now.getTime() + 8 * 3600000); return utcPlus8.toISOString().split('T')[0]; } getYesterdayKey() { const now = new Date(); const yesterday = new Date(now.getTime() - 24 * 3600000); const utcPlus8 = new Date(yesterday.getTime() + 8 * 3600000); return utcPlus8.toISOString().split('T')[0]; } loadFromStorage() { const raw = localStorage.getItem(this.storageKey); try { return raw ? JSON.parse(raw) : {}; } catch { return {}; } } saveToStorage() { localStorage.setItem(this.storageKey, JSON.stringify(this.data)); } setTodayValues(current, nonCurrent) { const roleData = this.getRoleData(); const today = this.getTodayKey(); roleData[today] = { currentAssets: current, nonCurrentAssets: nonCurrent, totalAssets: current + nonCurrent, timestamp: Date.now() }; this.cleanupOldData(); this.saveToStorage(); console.log(`[DEBUG] 存储当日数据: 当前资产=${current}, 非当前资产=${nonCurrent}, 时间=${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`); } cleanupOldData() { const roleData = this.getRoleData(); const keys = Object.keys(roleData).sort(); const cutoff = Date.now() - (this.maxDays * 24 * 3600 * 1000); const newData = {}; keys.forEach(key => { if (roleData[key].timestamp > cutoff) { newData[key] = roleData[key]; } }); this.data[this.currentRole] = newData; } getTodayDeltas() { const roleData = this.getRoleData(); const todayKey = this.getTodayKey(); const yesterdayKey = this.getYesterdayKey(); const todayData = roleData[todayKey] || { currentAssets: 0, nonCurrentAssets: 0, totalAssets: 0 }; const yesterdayData = roleData[yesterdayKey] || { currentAssets: 0, nonCurrentAssets: 0, totalAssets: 0 }; return { currentDelta: todayData.currentAssets - yesterdayData.currentAssets, nonCurrentDelta: todayData.nonCurrentAssets - yesterdayData.nonCurrentAssets, totalDelta: todayData.totalAssets - yesterdayData.totalAssets, totalRatio: yesterdayData.totalAssets > 0 ? (todayData.totalAssets - yesterdayData.totalAssets) / yesterdayData.totalAssets * 100 : 0 }; console.log(`[DEBUG] 差值计算: 今日数据=${JSON.stringify(todayData)}, 昨日数据=${JSON.stringify(yesterdayData)}`); } getHistoryData(days = 30) { const roleData = this.getRoleData(); const cutoff = Date.now() - (days * 24 * 3600 * 1000); const filtered = Object.entries(roleData) .filter(([_, data]) => data.timestamp > cutoff) .sort(([a], [b]) => new Date(a) - new Date(b)); return { labels: filtered.map(([date]) => date), currentAssets: filtered.map(([_, data]) => data.currentAssets), nonCurrentAssets: filtered.map(([_, data]) => data.nonCurrentAssets), totalAssets: filtered.map(([_, data]) => data.totalAssets) }; } getAllRoles() { return Object.keys(this.data); } removeRole(roleId) { delete this.data[roleId]; this.saveToStorage(); } } const store = new AssetDataStore(); let chart = null; const updateDisplay = (isFirst = false) => { const divElement = document.querySelector('.CharacterName_name__1amXp'); const username = divElement?.querySelector('span')?.textContent || 'default'; store.setRole(username); const totalAssets = currentAssets + nonCurrentAssets; store.setTodayValues(currentAssets, nonCurrentAssets); const deltas = store.getTodayDeltas(); const formattedTotalDelta = formatLargeNumber(deltas.totalDelta); const totalDeltaClass = deltas.totalDelta > 0 ? 'positive-delta' : (deltas.totalDelta < 0 ? 'negative-delta' : 'neutral-delta'); if (isFirst) { dom.insertAdjacentHTML('afterend', ` <div id="assetDeltaContainer" style="margin-top: 0px;"> <div class="asset-delta-display"> <span class="asset-delta-label">💰总资产增长:</span> <span class="${totalDeltaClass}">${formattedTotalDelta}</span> <span id="showHistoryIcon" title="显示详细资产历史图表">📊</span> </div> </div> `); // 创建弹窗 const modal = document.createElement('div'); modal.id = 'deltaNetworthChartModal'; modal.innerHTML = ` <div id="deltaNetworthChartHeader"> <span>详细资产历史曲线 (v${GM_info.script.version})</span> <span id="deltaNetworthChartCloseBtn" style="cursor:pointer;">❌</span> </div> <div id="chartOptionsContainer"> <div id="chartDisplayOptions"> <span style="margin-right:10px;font-weight:bold;">显示:</span> <span class="chart-option"> <input type="checkbox" id="showCurrentOption" checked> <label for="showCurrentOption">流动资产</label> </span> <span class="chart-option"> <input type="checkbox" id="showNonCurrentOption" checked> <label for="showNonCurrentOption">非流动资产</label> </span> <span class="chart-option"> <input type="checkbox" id="showTotalOption" checked> <label for="showTotalOption">总资产</label> </span> </div> <div id="timeRangeOptions"> <span style="margin-right:10px;font-weight:bold;">时间范围:</span> <button id="btn3Days" class="time-range-btn">3天</button> <button id="btn7Days" class="time-range-btn">7天</button> <button id="btn30Days" class="time-range-btn active">30天</button> <button id="btn60Days" class="time-range-btn">60天</button> <button id="btn90Days" class="time-range-btn">90天</button> <button id="btn180Days" class="time-range-btn">180天</button> </div> </div> <div id="netWorthChartBody"> <canvas id="netWorthChart"></canvas> </div> `; document.body.appendChild(modal); // 初始化图表选项 const options = getChartOptions(); document.getElementById('showCurrentOption').checked = options.showCurrent; document.getElementById('showNonCurrentOption').checked = options.showNonCurrent; document.getElementById('showTotalOption').checked = options.showTotal; // 设置活动的时间范围按钮 document.querySelectorAll('.time-range-btn').forEach(btn => { if (btn.id === `btn${options.daysToShow}Days`) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); // 事件监听 document.getElementById('showHistoryIcon').addEventListener('click', toggleModal); document.getElementById('deltaNetworthChartCloseBtn').addEventListener('click', hideModal); // 图表选项变化监听 document.getElementById('showCurrentOption').addEventListener('change', updateChartVisibility); document.getElementById('showNonCurrentOption').addEventListener('change', updateChartVisibility); document.getElementById('showTotalOption').addEventListener('change', updateChartVisibility); // 时间范围按钮监听 document.getElementById('btn3Days').addEventListener('click', () => updateChartTimeRange(3)); document.getElementById('btn7Days').addEventListener('click', () => updateChartTimeRange(7)); document.getElementById('btn30Days').addEventListener('click', () => updateChartTimeRange(30)); document.getElementById('btn60Days').addEventListener('click', () => updateChartTimeRange(60)); document.getElementById('btn90Days').addEventListener('click', () => updateChartTimeRange(90)); document.getElementById('btn180Days').addEventListener('click', () => updateChartTimeRange(180)); // 拖动功能 setupDrag(modal); } else { const container = document.getElementById('assetDeltaContainer'); if (container) { container.innerHTML = ` <div class="asset-delta-display"> <span class="asset-delta-label">💰总资产增长:</span> <span class="${totalDeltaClass}">${formattedTotalDelta}</span> <span id="showHistoryIcon" title="显示详细资产历史图表">📊</span> </div> `; document.getElementById('showHistoryIcon').addEventListener('click', toggleModal); } } }; function toggleModal() { const modal = document.getElementById('deltaNetworthChartModal'); if (modal.style.display === 'flex') { hideModal(); } else { showModal(); } } function showModal() { const modal = document.getElementById('deltaNetworthChartModal'); modal.style.display = 'flex'; if (!window.Chart) { loadChartLibrary().then(initializeChart); } else if (!chart) { initializeChart(); } else { updateChart(); } } function hideModal() { const modal = document.getElementById('deltaNetworthChartModal'); modal.style.display = 'none'; // 重置弹窗位置 modal.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 850px; max-width: 90vw; background: #1e1e1e; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.6); z-index: 9999; display: none; flex-direction: column; `; } function loadChartLibrary() { return new Promise((resolve) => { const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js'; script.onload = resolve; document.head.appendChild(script); }); } function initializeChart() { const options = getChartOptions(); const historyData = store.getHistoryData(options.daysToShow); const ctx = document.getElementById('netWorthChart').getContext('2d'); chart = new Chart(ctx, { type: 'line', data: { labels: historyData.labels, datasets: [ { id: 'current', label: '流动资产', data: historyData.currentAssets, borderColor: 'rgba(75, 192, 192, 1)', backgroundColor: 'rgba(75, 192, 192, 0.1)', tension: 0.3, fill: false, hidden: !options.showCurrent }, { id: 'nonCurrent', label: '非流动资产', data: historyData.nonCurrentAssets, borderColor: 'rgba(255, 99, 132, 1)', backgroundColor: 'rgba(255, 99, 132, 0.1)', tension: 0.3, fill: false, hidden: !options.showNonCurrent }, { id: 'total', label: '总资产', data: historyData.totalAssets, borderColor: 'rgba(54, 162, 235, 1)', backgroundColor: 'rgba(54, 162, 235, 0.1)', tension: 0.3, fill: false, hidden: !options.showTotal } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'top', labels: { usePointStyle: true, boxWidth: 10 } }, tooltip: { callbacks: { label: (context) => { const label = context.dataset.label || ''; const value = formatLargeNumber(context.raw); return `${label}: ${value}`; } } } }, scales: { y: { ticks: { callback: (value) => formatLargeNumber(value) } } } } }); } function updateChart() { const options = getChartOptions(); const historyData = store.getHistoryData(options.daysToShow); chart.data.labels = historyData.labels; chart.data.datasets[0].data = historyData.currentAssets; chart.data.datasets[1].data = historyData.nonCurrentAssets; chart.data.datasets[2].data = historyData.totalAssets; chart.update(); } function updateChartVisibility() { const options = { showCurrent: document.getElementById('showCurrentOption').checked, showNonCurrent: document.getElementById('showNonCurrentOption').checked, showTotal: document.getElementById('showTotalOption').checked, daysToShow: getChartOptions().daysToShow }; saveChartOptions(options); if (chart) { chart.data.datasets[0].hidden = !options.showCurrent; chart.data.datasets[1].hidden = !options.showNonCurrent; chart.data.datasets[2].hidden = !options.showTotal; chart.update(); } } function updateChartTimeRange(days) { const options = getChartOptions(); options.daysToShow = days; saveChartOptions(options); // 更新活动按钮样式 document.querySelectorAll('.time-range-btn').forEach(btn => { if (btn.id === `btn${days}Days`) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); if (chart) { const historyData = store.getHistoryData(days); chart.data.labels = historyData.labels; chart.data.datasets[0].data = historyData.currentAssets; chart.data.datasets[1].data = historyData.nonCurrentAssets; chart.data.datasets[2].data = historyData.totalAssets; chart.update(); } } function setupDrag(modal) { let isDragging = false; let startX, startY, initialLeft, initialTop; modal.querySelector('#deltaNetworthChartHeader').addEventListener('mousedown', (e) => { isDragging = true; // 获取初始鼠标位置和弹窗位置 startX = e.clientX; startY = e.clientY; // 获取当前弹窗位置(从样式或计算位置) const rect = modal.getBoundingClientRect(); initialLeft = rect.left; initialTop = rect.top; modal.classList.add('dragging'); e.preventDefault(); // 防止文本选中 }); document.addEventListener('mousemove', (e) => { if (isDragging) { // 计算鼠标移动距离 const dx = e.clientX - startX; const dy = e.clientY - startY; // 应用新的位置 modal.style.left = `${initialLeft + dx}px`; modal.style.top = `${initialTop + dy}px`; modal.style.transform = 'none'; } }); document.addEventListener('mouseup', () => { isDragging = false; modal.classList.remove('dragging'); }); } // 初始更新 updateDisplay(true); setInterval(() => updateDisplay(false), 10 * 60 * 1000); // 每10分钟刷新 }; // 检查资产元素并运行脚本 const checkAssetsAndRun = () => { // 获取各个组成部分的值 const equippedNetworth = parseFormattedNumber(document.querySelector('#equippedNetworthAsk')?.textContent?.trim() || '0'); const inventoryNetworth = parseFormattedNumber(document.querySelector('#inventoryNetworthAsk')?.textContent?.trim() || '0'); const marketListingsNetworth = parseFormattedNumber(document.querySelector('#marketListingsNetworthAsk')?.textContent?.trim() || '0'); const totalHouseScore = parseFormattedNumber(document.querySelector('#totalHouseScore')?.textContent?.trim() || '0'); const abilityScore = parseFormattedNumber(document.querySelector('#abilityScore')?.textContent?.trim() || '0'); // 计算新的资产值 const currentAssets = equippedNetworth + inventoryNetworth + marketListingsNetworth; const nonCurrentAssets = totalHouseScore + abilityScore; const insertDom = document.getElementById('netWorthDetails'); if (insertDom && !document.getElementById('assetDeltaContainer')) { window.kbd_calculateTotalNetworth?.(currentAssets, nonCurrentAssets, insertDom); /* const currentAssetsElement = document.querySelector('#currentAssets'); const nonCurrentAssetsElement = document.querySelector('#nonCurrentAssets'); if (currentAssetsElement && nonCurrentAssetsElement) { const currentAssets = parseFormattedNumber(currentAssetsElement.textContent.trim()); const nonCurrentAssets = parseFormattedNumber(nonCurrentAssetsElement.textContent.trim()); const insertDom = document.getElementById('netWorthDetails'); if (insertDom && !document.getElementById('assetDeltaContainer')) { window.kbd_calculateTotalNetworth?.(currentAssets, nonCurrentAssets, insertDom); } */ } }; // 初始检查和定时检查 checkAssetsAndRun(); setInterval(checkAssetsAndRun, 5000); })();