每日总资产增长(DailyAssets)

记录每日总资产增长,图表中分别显示总资产、流动资产、非流动资产详情,数据存储在本地,可查看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);
})();