记录每日总资产增长,包含详细统计功能:当前资产、日环比、瞬时时薪、近7天均增速、近7天胜率、近30天日均、最佳/最差日、上一次翻倍等,支持数据导入导出(独立版,不依赖MWITools)
// ==UserScript==
// @name DailyAssets Plus
// @namespace http://tampermonkey.net/
// @version 1.0.7
// @description 记录每日总资产增长,包含详细统计功能:当前资产、日环比、瞬时时薪、近7天均增速、近7天胜率、近30天日均、最佳/最差日、上一次翻倍等,支持数据导入导出(独立版,不依赖MWITools)
// @author Vicky718 (基于 VictoryWinWinWin, PaperCat, SuXingX 的代码增强)
// @match https://www.milkywayidle.com/*
// @match https://www.milkywayidlecn.com/*
// @match https://test.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';
/* =========================
语言配置
========================= */
const LANGUAGES = {
zh: {
// 基础UI
assetsGrowth: '💰总资产增长',
historyIcon: '显示详细资产历史图表',
metricsIconShow: '显示统计指标',
metricsIconHide: '隐藏统计指标',
settingsIcon: '图表设置',
// 额外信息
avg7Days: '近7天日均',
lastRecord: '最近记录',
// 图表弹窗
chartTitle: '资产历史曲线',
timeRange: '时间范围:',
// 设置弹窗
settingsTitle: '资产图表设置',
viewMode: '视图模式',
summaryView: '摘要视图',
detailedView: '详细视图',
// 摘要视图选项
summaryOptions: '摘要视图显示选项',
showCurrent: '显示流动资产',
showNonCurrent: '显示非流动资产',
// 详细视图选项
detailedOptions: '详细视图显示选项',
showEquipped: '显示装备价值',
showInventory: '显示库存价值',
showMarket: '显示订单价值',
showHouse: '显示房子价值',
showAbility: '显示技能价值',
// 通用选项
generalOptions: '通用显示选项',
showTotal: '显示总资产',
// 单位设置
unitSettings: '资产显示单位',
unitMode: '单位模式:',
unitAuto: '自动',
unitK: '千(K)',
unitM: '百万(M)',
unitB: '十亿(B)',
// 时间范围设置
timeRangeSettings: '时间范围设置',
showTimeRange: '显示时间范围选择器',
availableTimeRanges: '可用时间范围:',
currentButtons: '当前可用的时间范围按钮:',
// 导入导出
importExport: '📦 数据导入导出',
exportAll: '导出全部(含设置)',
exportData: '仅导出数据',
importData: '导入数据',
// 导入预览
preview: '导入预览',
version: '版本',
exportTime: '导出时间',
roleCount: '角色数量',
totalRecords: '总记录数',
containsSettings: '包含设置',
yes: '是',
no: '否',
roleStats: '角色数据统计:',
records: '条记录',
confirmImport: '确认导入',
cancel: '取消',
// 通知
exportSuccess: '数据已导出到',
exportFailed: '导出失败',
importFailed: '导入失败',
noImportData: '没有可导入的数据',
invalidFormat: '无效的数据格式',
unrecognizedVersion: '无法识别的备份文件版本',
missingAssetData: '备份文件中缺少资产数据',
previewFailed: '预览失败',
// 统计指标
currentAssets: '当前资产',
recentUpdate: '最近更新',
dailyChange: '日环比',
hourlyRate: '瞬时时薪',
hourlyRateDesc: '按当日平均',
avg7Growth: '近7天均增速',
avg7Desc: '日均增率 / 日均净变动',
winRate7: '近7天胜率',
avg30Daily: '近30天日均',
bestWorstDay: '最佳/最差日',
lastDouble: '上一次翻倍',
noRecord: '尚无记录',
doubling: '翻倍',
target: '目标',
days: '天',
profitLossFlat: '盈/亏/平',
// 价值名称
equipmentValue: '装备价值',
inventoryValue: '库存价值',
marketValue: '订单价值',
houseValue: '房子价值',
abilityValue: '技能价值',
currentAssetsValue: '流动资产',
nonCurrentAssetsValue: '非流动资产',
totalAssets: '总资产',
},
en: {
// Base UI
assetsGrowth: '💰Total Asset Growth',
historyIcon: 'Show asset history chart',
metricsIconShow: 'Show statistics',
metricsIconHide: 'Hide statistics',
settingsIcon: 'Chart settings',
// Extra info
avg7Days: '7-day avg',
lastRecord: 'Last record',
// Chart modal
chartTitle: 'Asset History Chart',
timeRange: 'Time range:',
// Settings modal
settingsTitle: 'Asset Chart Settings',
viewMode: 'View Mode',
summaryView: 'Summary View',
detailedView: 'Detailed View',
// Summary view options
summaryOptions: 'Summary View Options',
showCurrent: 'Show Current Assets',
showNonCurrent: 'Show Non-Current Assets',
// Detailed view options
detailedOptions: 'Detailed View Options',
showEquipped: 'Show Equipment Value',
showInventory: 'Show Inventory Value',
showMarket: 'Show Market Value',
showHouse: 'Show House Value',
showAbility: 'Show Ability Value',
// General options
generalOptions: 'General Options',
showTotal: 'Show Total Assets',
// Unit settings
unitSettings: 'Asset Display Unit',
unitMode: 'Unit mode:',
unitAuto: 'Auto',
unitK: 'K',
unitM: 'M',
unitB: 'B',
// Time range settings
timeRangeSettings: 'Time Range Settings',
showTimeRange: 'Show time range selector',
availableTimeRanges: 'Available time ranges:',
currentButtons: 'Current time range buttons:',
// Import/Export
importExport: '📦 Data Import/Export',
exportAll: 'Export All (with settings)',
exportData: 'Export Data Only',
importData: 'Import Data',
// Import preview
preview: 'Import Preview',
version: 'Version',
exportTime: 'Export time',
roleCount: 'Characters',
totalRecords: 'Total records',
containsSettings: 'Contains settings',
yes: 'Yes',
no: 'No',
roleStats: 'Character statistics:',
records: 'records',
confirmImport: 'Confirm Import',
cancel: 'Cancel',
// Notifications
exportSuccess: 'Data exported to',
exportFailed: 'Export failed',
importFailed: 'Import failed',
noImportData: 'No data to import',
invalidFormat: 'Invalid data format',
unrecognizedVersion: 'Unrecognized backup version',
missingAssetData: 'Missing asset data in backup',
previewFailed: 'Preview failed',
// Metrics
currentAssets: 'Current Assets',
recentUpdate: 'Last update',
dailyChange: 'Daily Change',
hourlyRate: 'Hourly Rate',
hourlyRateDesc: 'Based on daily average',
avg7Growth: '7-day Avg Growth',
avg7Desc: 'Avg rate / Avg change',
winRate7: '7-day Win Rate',
avg30Daily: '30-day Daily Avg',
bestWorstDay: 'Best/Worst Day',
lastDouble: 'Last Double',
noRecord: 'No record',
doubling: 'Double in',
target: 'Target',
days: 'd',
profitLossFlat: 'Win/Loss/Flat',
// Value names
equipmentValue: 'Equipment Value',
inventoryValue: 'Inventory Value',
marketValue: 'Market Value',
houseValue: 'House Value',
abilityValue: 'Ability Value',
currentAssetsValue: 'Current Assets',
nonCurrentAssetsValue: 'Non-Current Assets',
totalAssets: 'Total Assets',
}
};
// 获取当前语言
function getCurrentLanguage() {
// 从设置中获取语言偏好,默认为中文
const lang = GM_getValue('dailyAssetsLanguage', 'zh');
console.log('[DailyAssets] Current language:', lang);
return lang === 'en' ? LANGUAGES.en : LANGUAGES.zh;
}
// 切换语言
function toggleLanguage() {
const currentLang = GM_getValue('dailyAssetsLanguage', 'zh');
const newLang = currentLang === 'zh' ? 'en' : 'zh';
console.log('[DailyAssets] Switching language from', currentLang, 'to', newLang);
GM_setValue('dailyAssetsLanguage', newLang);
// 延迟刷新,确保值已保存
setTimeout(() => {
location.reload();
}, 100);
}
// 获取当前语言的文本
function t(key) {
const lang = getCurrentLanguage();
return lang[key] || key;
}
/* =========================
常量与存储键定义
========================= */
const STORAGE_KEYS = {
assetData: 'kbd_asset_data_v2',
metricsPrefs: 'kbd_metrics_prefs',
metricsPanel: 'kbd_metrics_panel',
lastUpdate: 'kbd_last_update_at',
};
/* =========================
样式定义(添加语言切换按钮样式)
========================= */
GM_addStyle(`
/* 基础样式 */
.asset-delta-display {
text-align: left;
color: #fff;
font-size: 16px;
margin: 0px 0;
}
.asset-delta-label {
font-weight: bold;
margin-right: 5px;
}
#showHistoryIcon, #showMetricsIcon, #languageToggle {
cursor: pointer;
margin-left: 8px;
font-size: 16px;
display: inline-block;
margin-top: 0px;
opacity: 0.8;
transition: opacity 0.2s;
}
#showHistoryIcon:hover, #showMetricsIcon:hover, #languageToggle:hover {
opacity: 1;
}
#languageToggle {
color: #FFD700;
}
#settingsToggle {
cursor: pointer;
margin-left: 8px;
font-size: 16px;
color: #2196F3;
opacity: 0.8;
transition: opacity 0.2s;
}
#settingsToggle:hover {
opacity: 1;
}
.positive-delta {
color: #4CAF50;
font-weight: bold;
}
.negative-delta {
color: #F44336;
font-weight: bold;
}
.neutral-delta {
color: #9E9E9E;
font-weight: bold;
}
/* 统计指标面板 */
.ep-metrics-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
padding: 10px 15px;
background: #111b2b;
border-top: 1px solid rgba(255,255,255,0.05);
border-bottom: 1px solid rgba(255,255,255,0.05);
}
@media (max-width: 720px) {
.ep-metrics-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 420px) {
.ep-metrics-grid { grid-template-columns: 1fr; }
}
.ep-metric-card {
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 8px;
padding: 8px 10px;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.ep-metric-card h4 {
font-size: 12px;
font-weight: normal;
color: #9fb4d1;
margin: 0;
}
.ep-metric-card strong {
font-size: 18px;
color: #f7fafc;
word-break: break-word;
}
.ep-metric-card span {
font-size: 12px;
color: #7f8ca3;
word-break: break-word;
}
/* 额外信息行 */
.ep-delta-extra {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 6px;
font-size: 14px;
color: #cfd8e3;
}
.ep-delta-extra span {
background: rgba(255, 255, 255, 0.08);
border-radius: 4px;
padding: 3px 8px;
}
/* 弹窗样式 */
#deltaNetworthChartModal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 1200px;
max-width: 95vw;
background: #1e1e1e;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.6);
z-index: 10000;
display: none;
flex-direction: column;
color: #f5f5f5;
border: 1px solid rgba(255,255,255,0.08);
}
#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: move;
user-select: none;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
#netWorthChartBody {
padding: 15px;
background: #0b1522;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
border: 1px solid rgba(255,255,255,0.05);
}
#netWorthChart {
width: 100%;
height: 400px;
background: radial-gradient(circle at top, rgba(0,198,255,0.08), rgba(2,12,24,0.95));
border-radius: 6px;
}
/* 设置弹窗样式 */
#assetSettingsModal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 600px;
max-width: 95vw;
background: #1e1e1e;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.6);
z-index: 10001;
display: none;
flex-direction: column;
}
#assetSettingsModal.dragging {
cursor: grabbing;
}
#assetSettingsHeader {
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;
}
#assetSettingsBody {
padding: 15px;
max-height: 70vh;
overflow-y: auto;
}
/* 模态遮罩 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 9999;
display: none;
}
/* 关闭按钮 */
.close-btn {
cursor: pointer;
font-size: 18px;
color: #fff;
}
.close-btn:hover {
color: #f44336;
}
/* 视图切换 */
.view-toggle {
display: flex;
background: #333;
border-radius: 4px;
padding: 2px;
margin-bottom: 15px;
}
.view-option {
flex: 1;
text-align: center;
padding: 8px;
cursor: pointer;
border-radius: 3px;
font-weight: bold;
transition: all 0.3s ease;
}
.view-option.active {
background: #4CAF50;
color: white;
}
.view-option:not(.active) {
background: transparent;
color: #ccc;
}
.view-option:not(.active):hover {
background: #444;
}
/* 设置区域 */
.settings-section {
background: #2a2a2a;
padding: 15px;
margin: 2px 0;
border-radius: 4px;
}
.settings-title {
color: #fff;
font-weight: bold;
font-size: 16px;
margin-bottom: 5px;
display: block;
}
.settings-group {
color: #fff;
margin-bottom: 15px;
}
.chart-option {
margin: 5px 0;
display: flex;
align-items: center;
}
.chart-option input {
margin-right: 8px;
}
.chart-option label {
cursor: pointer;
color: white;
flex-grow: 1;
}
/* 时间范围按钮 */
.time-range-btn {
padding: 5px 10px;
background: rgba(0,0,0,.3);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 5px;
margin-bottom: 5px;
}
.time-range-btn:hover {
background: #555;
}
.time-range-btn.active {
background: rgb(25, 118, 210);
font-weight: bold;
}
.time-range-section {
margin: 15px 0;
}
/* 图表选项容器 */
#chartOptionsContainer {
padding: 4px;
background: #111b2b;
border-bottom: 0px solid #333;
}
#timeRangeOptions {
margin-top: 10px;
color: #fff;
}
#timeRangeOptions.hidden {
display: none;
}
.time-range-buttons {
margin-top: 10px;
}
/* 单位设置 */
.unit-toggle {
display: flex;
background: #333;
border-radius: 4px;
padding: 2px;
margin-top: 8px;
}
.unit-option {
flex: 1;
text-align: center;
padding: 6px;
cursor: pointer;
border-radius: 3px;
font-weight: bold;
font-size: 14px;
transition: all 0.3s ease;
}
.unit-option.active {
background: #2196F3;
color: white;
}
.unit-option:not(.active) {
background: transparent;
color: #ccc;
}
.unit-option:not(.active):hover {
background: #444;
}
/* 导入导出样式 */
.import-export-section {
background: #2a2a2a;
padding: 15px;
margin: 2px 0;
border-radius: 4px;
}
.import-export-buttons {
display: flex;
gap: 10px;
margin-top: 10px;
flex-wrap: wrap;
}
.import-export-btn {
padding: 8px 15px;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
flex: 1;
min-width: 100px;
}
.import-export-btn:hover {
background: #1976D2;
}
.import-export-btn.export-all {
background: #4CAF50;
}
.import-export-btn.export-all:hover {
background: #45a049;
}
.import-export-btn.import {
background: #FF9800;
}
.import-export-btn.import:hover {
background: #F57C00;
}
.file-input-hidden {
display: none;
}
.import-preview {
margin-top: 15px;
padding: 10px;
background: #333;
border-radius: 4px;
display: none;
}
.import-preview.show {
display: block;
}
.import-preview-title {
color: #fff;
font-weight: bold;
margin-bottom: 10px;
}
.import-preview-content {
max-height: 200px;
overflow-y: auto;
color: #ccc;
font-size: 12px;
}
.import-preview-actions {
display: flex;
gap: 10px;
margin-top: 10px;
}
.confirm-import-btn {
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
}
.cancel-import-btn {
background: #f44336;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 10px 20px;
background: #333;
color: white;
border-radius: 4px;
z-index: 10002;
animation: slideIn 0.3s ease;
}
.notification.success {
background: #4CAF50;
}
.notification.error {
background: #f44336;
}
.notification.info {
background: #2196F3;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
`);
/* =========================
工具函数
========================= */
const safeJsonParse = (raw, fallback) => {
try { return raw ? JSON.parse(raw) : fallback; } catch { return fallback; }
};
const readPrefs = (key) => safeJsonParse(localStorage.getItem(key), {});
const writePrefs = (key, prefs) => localStorage.setItem(key, JSON.stringify(prefs));
const getRoleBoolPref = (key, roleId, defaultValue) => {
const prefs = readPrefs(key);
if (roleId && Object.prototype.hasOwnProperty.call(prefs, roleId)) return !!prefs[roleId];
return !!defaultValue;
};
const setRoleBoolPref = (key, roleId, value) => {
if (!roleId) return;
const prefs = readPrefs(key);
prefs[roleId] = !!value;
writePrefs(key, prefs);
};
const readRoleLastUpdateMap = () => safeJsonParse(localStorage.getItem(STORAGE_KEYS.lastUpdate), {});
const writeRoleLastUpdateMap = (map) => localStorage.setItem(STORAGE_KEYS.lastUpdate, JSON.stringify(map || {}));
const setRoleLastUpdate = (roleId, iso = new Date().toISOString()) => {
if (!roleId) return;
const map = readRoleLastUpdateMap();
map[roleId] = iso;
writeRoleLastUpdateMap(map);
};
const getRoleLastUpdate = (roleId) => {
if (!roleId) return null;
const map = readRoleLastUpdateMap();
return map && map[roleId] ? map[roleId] : null;
};
const formatIsoToLocalDateTime = (iso) => {
if (!iso) return '';
const d = new Date(iso);
if (!Number.isFinite(d.getTime())) return String(iso);
const pad = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
};
// 解析带格式的数字
function parseFormattedNumber(str) {
if (!str) return 0;
const match = String(str).match(/(-?[\d.,]+)\s*([kKmMbBtT]?)/);
if (!match) return 0;
let [, numericPart, unit = ''] = match;
numericPart = numericPart.replace(/\s+/g, '');
if (!numericPart) return 0;
const commaCount = (numericPart.match(/,/g) || []).length;
const dotCount = (numericPart.match(/\./g) || []).length;
if (commaCount && dotCount) {
if (numericPart.lastIndexOf('.') > numericPart.lastIndexOf(',')) {
numericPart = numericPart.replace(/,/g, '');
} else {
numericPart = numericPart.replace(/\./g, '');
numericPart = numericPart.replace(/,/g, '.');
}
} else if (commaCount) {
if (commaCount === 1 && numericPart.split(',')[1]?.length <= 2) numericPart = numericPart.replace(',', '.');
else numericPart = numericPart.replace(/,/g, '');
} else if (dotCount > 1) {
const parts = numericPart.split('.');
const decimal = parts.pop();
numericPart = parts.join('') + (decimal ? `.${decimal}` : '');
}
const num = parseFloat(numericPart);
if (isNaN(num)) return 0;
const multiplierMap = { k: 1e3, m: 1e6, b: 1e9, t: 1e12 };
const multiplier = multiplierMap[(unit || '').toLowerCase()] || 1;
return num * multiplier;
}
/* =========================
格式化大数字函数(支持单位模式控制)
========================= */
function formatLargeNumber(num, unitMode = 'auto') {
const n = Number(num) || 0;
const abs = Math.abs(n);
// 根据单位模式格式化
switch(unitMode) {
case 'k':
return (n / 1e3).toFixed(2) + 'K';
case 'm':
return (n / 1e6).toFixed(2) + 'M';
case 'b':
return (n / 1e9).toFixed(2) + 'B';
case 'auto':
default:
if (abs >= 1e12) return (n / 1e12).toFixed(2) + 'T';
if (abs >= 1e9) return (n / 1e9).toFixed(2) + 'B';
if (abs >= 1e6) return (n / 1e6).toFixed(2) + 'M';
if (abs >= 1e3) return (n / 1e3).toFixed(2) + 'K';
return n.toFixed(2);
}
}
const formatSignedLargeNumber = (num, unitMode = 'auto') => {
const n = Number(num) || 0;
return n > 0 ? `+${formatLargeNumber(n, unitMode)}` : formatLargeNumber(n, unitMode);
};
/* =========================
获取MWITools显示的数据
========================= */
function getMWIToolsValues() {
return new Promise((resolve) => {
// 等待netWorthDetails出现
const checkElement = () => {
const netWorthDetails = document.getElementById('netWorthDetails');
if (netWorthDetails && netWorthDetails.children.length > 0) {
// 获取所有显示的数值
const values = {
equippedNetworth: 0,
inventoryNetworth: 0,
marketListingsNetworth: 0,
totalHouseScore: 0,
abilityScore: 0
};
try {
// 获取所有div元素
const divs = netWorthDetails.querySelectorAll('div');
// 遍历所有div,根据文本内容匹配
divs.forEach(div => {
const text = div.textContent;
// 匹配装备价值
if (text.includes('装备价值') || text.includes('Equipment value')) {
const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/);
if (match) values.equippedNetworth = parseFormattedNumber(match[1]);
}
// 匹配库存价值
else if (text.includes('库存价值') || text.includes('Inventory value')) {
const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/);
if (match) values.inventoryNetworth = parseFormattedNumber(match[1]);
}
// 匹配订单价值
else if (text.includes('订单价值') || text.includes('Market listing value')) {
const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/);
if (match) values.marketListingsNetworth = parseFormattedNumber(match[1]);
}
// 匹配房子价值
else if (text.includes('房子价值') || text.includes('Houses value')) {
const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/);
if (match) values.totalHouseScore = parseFormattedNumber(match[1]);
}
// 匹配技能价值
else if (text.includes('技能价值') || text.includes('Abilities value')) {
const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/);
if (match) values.abilityScore = parseFormattedNumber(match[1]);
}
});
// 如果没找到具体数值,尝试从currentAssets和nonCurrentAssets中提取
const currentAssets = document.querySelector('#currentAssets');
const nonCurrentAssets = document.querySelector('#nonCurrentAssets');
if (currentAssets) {
const currentDivs = currentAssets.querySelectorAll('div');
currentDivs.forEach(div => {
const text = div.textContent;
if (text.includes('装备价值') || text.includes('Equipment value')) {
const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/);
if (match) values.equippedNetworth = parseFormattedNumber(match[1]);
} else if (text.includes('库存价值') || text.includes('Inventory value')) {
const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/);
if (match) values.inventoryNetworth = parseFormattedNumber(match[1]);
} else if (text.includes('订单价值') || text.includes('Market listing value')) {
const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/);
if (match) values.marketListingsNetworth = parseFormattedNumber(match[1]);
}
});
}
if (nonCurrentAssets) {
const nonCurrentDivs = nonCurrentAssets.querySelectorAll('div');
nonCurrentDivs.forEach(div => {
const text = div.textContent;
if (text.includes('房子价值') || text.includes('Houses value')) {
const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/);
if (match) values.totalHouseScore = parseFormattedNumber(match[1]);
} else if (text.includes('技能价值') || text.includes('Abilities value')) {
const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/);
if (match) values.abilityScore = parseFormattedNumber(match[1]);
}
});
}
// 如果还是没找到,尝试从总文本中提取数字
if (values.equippedNetworth === 0 && values.inventoryNetworth === 0 &&
values.marketListingsNetworth === 0 && values.totalHouseScore === 0 &&
values.abilityScore === 0) {
const allText = netWorthDetails.textContent;
const numbers = allText.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/g) || [];
if (numbers.length >= 5) {
values.equippedNetworth = parseFormattedNumber(numbers[0]);
values.inventoryNetworth = parseFormattedNumber(numbers[1]);
values.marketListingsNetworth = parseFormattedNumber(numbers[2]);
values.totalHouseScore = parseFormattedNumber(numbers[3]);
values.abilityScore = parseFormattedNumber(numbers[4]);
}
}
console.log('[DailyAssets] 获取到的资产价值:', values);
resolve(values);
} catch (error) {
console.error('[DailyAssets] 解析价值时出错:', error);
resolve({
equippedNetworth: 0,
inventoryNetworth: 0,
marketListingsNetworth: 0,
totalHouseScore: 0,
abilityScore: 0
});
}
} else {
// 没找到元素,继续等待
setTimeout(checkElement, 1000);
}
};
checkElement();
});
}
/* =========================
数据存储类
========================= */
class AssetDataStore {
constructor(storageKey = STORAGE_KEYS.assetData, 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() {
return safeJsonParse(localStorage.getItem(this.storageKey), {});
}
saveToStorage() {
localStorage.setItem(this.storageKey, JSON.stringify(this.data));
}
setTodayDetailedValues(equipped, inventory, market, house, ability) {
const roleData = this.getRoleData();
const today = this.getTodayKey();
const totalAssets = equipped + inventory + market + house + ability;
const currentAssets = equipped + inventory + market;
const nonCurrentAssets = house + ability;
roleData[today] = {
equippedNetworth: equipped,
inventoryNetworth: inventory,
marketListingsNetworth: market,
totalHouseScore: house,
abilityScore: ability,
currentAssets: currentAssets,
nonCurrentAssets: nonCurrentAssets,
totalAssets: totalAssets,
timestamp: Date.now()
};
this.cleanupOldData();
this.saveToStorage();
}
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] || {
equippedNetworth: 0, inventoryNetworth: 0, marketListingsNetworth: 0,
totalHouseScore: 0, abilityScore: 0,
currentAssets: 0, nonCurrentAssets: 0, totalAssets: 0
};
const yesterdayData = roleData[yesterdayKey] || {
equippedNetworth: 0, inventoryNetworth: 0, marketListingsNetworth: 0,
totalHouseScore: 0, abilityScore: 0,
currentAssets: 0, nonCurrentAssets: 0, totalAssets: 0
};
return {
equippedDelta: todayData.equippedNetworth - yesterdayData.equippedNetworth,
inventoryDelta: todayData.inventoryNetworth - yesterdayData.inventoryNetworth,
marketDelta: todayData.marketListingsNetworth - yesterdayData.marketListingsNetworth,
houseDelta: todayData.totalHouseScore - yesterdayData.totalHouseScore,
abilityDelta: todayData.abilityScore - yesterdayData.abilityScore,
totalDelta: todayData.totalAssets - yesterdayData.totalAssets,
totalRatio: yesterdayData.totalAssets > 0 ?
(todayData.totalAssets - yesterdayData.totalAssets) / yesterdayData.totalAssets * 100 : 0
};
}
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),
equippedNetworth: filtered.map(([_, data]) => data.equippedNetworth),
inventoryNetworth: filtered.map(([_, data]) => data.inventoryNetworth),
marketListingsNetworth: filtered.map(([_, data]) => data.marketListingsNetworth),
totalHouseScore: filtered.map(([_, data]) => data.totalHouseScore),
abilityScore: filtered.map(([_, data]) => data.abilityScore),
currentAssets: filtered.map(([_, data]) => data.currentAssets),
nonCurrentAssets: filtered.map(([_, data]) => data.nonCurrentAssets),
totalAssets: filtered.map(([_, data]) => data.totalAssets)
};
}
getHistoryEntriesSorted() {
const roleData = this.getRoleData();
return Object.entries(roleData)
.filter(([_, data]) => data.totalAssets !== undefined)
.map(([date, data]) => [date, data.totalAssets])
.sort(([a], [b]) => new Date(a) - new Date(b));
}
getAllRoles() {
return Object.keys(this.data);
}
removeRole(roleId) {
delete this.data[roleId];
this.saveToStorage();
}
}
/* =========================
统计指标计算函数
========================= */
const computeDeltas = (sortedEntries) => {
const diff = [];
for (let i = 1; i < sortedEntries.length; i++) {
const prev = sortedEntries[i - 1][1];
const curr = sortedEntries[i][1];
diff.push({
date: sortedEntries[i][0],
value: curr - prev,
growthPct: prev ? ((curr - prev) / prev) * 100 : 0,
});
}
return diff;
};
const computeStreaks = (differences) => {
let bestGain = 0;
let worstLoss = 0;
let bestDay = null;
let worstDay = null;
let winStreak = 0;
let loseStreak = 0;
let currentWin = 0;
let currentLose = 0;
differences.forEach((d) => {
if (d.value >= 0) {
currentWin += 1;
currentLose = 0;
if (d.value > bestGain) {
bestGain = d.value;
bestDay = d.date;
}
} else {
currentLose += 1;
currentWin = 0;
if (d.value < worstLoss) {
worstLoss = d.value;
worstDay = d.date;
}
}
winStreak = Math.max(winStreak, currentWin);
loseStreak = Math.max(loseStreak, currentLose);
});
return { bestGain, bestDay, worstLoss, worstDay, winStreak, loseStreak };
};
const predictDoublingTime = (differences, currentValue, windowDays = 7) => {
if (!currentValue || differences.length === 0) return null;
const recent = differences.slice(-windowDays);
if (!recent.length) return null;
const avgGrowth = recent.reduce((sum, d) => sum + d.value, 0) / recent.length;
if (avgGrowth <= 0) return null;
return Math.ceil((currentValue) / avgGrowth);
};
const predictTargetDate = (differences, currentValue, targetValue) => {
if (!currentValue || currentValue >= targetValue) {
return { days: 0, targetValue };
}
const recent = differences.slice(-7);
const avgGrowth = recent.reduce((sum, d) => sum + d.value, 0) / (recent.length || 1);
if (avgGrowth <= 0) return null;
const remaining = targetValue - currentValue;
return { days: Math.ceil(remaining / avgGrowth), targetValue };
};
const nextRoundNumber = (value) => {
if (!value) return 0;
const magnitude = Math.pow(10, Math.max(3, Math.floor(Math.log10(value))));
return Math.ceil(value / magnitude) * magnitude;
};
const computeTotalMetricsFromEntries = (sortedEntries, unitMode = 'auto') => {
const latestRecordDate = sortedEntries.length ? sortedEntries[sortedEntries.length - 1][0] : '-';
const valueToPersist = sortedEntries.length ? (sortedEntries[sortedEntries.length - 1][1] || 0) : 0;
const differences = computeDeltas(sortedEntries);
const todayDelta = differences.length ? differences[differences.length - 1] : null;
const growthPct = todayDelta ? (todayDelta.growthPct || 0) : 0;
const hourlyRate = todayDelta ? (todayDelta.value / 24) : 0;
const last7 = differences.slice(-7);
const avgGrowthPct = last7.length ? last7.reduce((sum, d) => sum + (d.growthPct || 0), 0) / last7.length : 0;
const avgGrowthValue = last7.length ? last7.reduce((sum, d) => sum + (d.value || 0), 0) / last7.length : 0;
const streaks = computeStreaks(differences);
const doublingDays = predictDoublingTime(differences, valueToPersist);
const targetValue = nextRoundNumber(valueToPersist * 1.05);
const targetPrediction = predictTargetDate(differences, valueToPersist, targetValue);
let lastDoubleDate = null;
let lastDoubleDays = null;
let lastDoubleValue = null;
if (valueToPersist > 0 && sortedEntries.length) {
const halfValue = valueToPersist / 2;
const milestoneEntry = sortedEntries.find(([, v]) => Number.isFinite(v) && v >= halfValue);
if (milestoneEntry) {
lastDoubleDate = milestoneEntry[0];
lastDoubleValue = milestoneEntry[1];
const diffMs = Date.now() - new Date(lastDoubleDate).getTime();
lastDoubleDays = Math.max(0, Math.floor(diffMs / 86400000));
}
}
return {
latestRecordDate,
valueToPersist,
differences,
todayDelta,
growthPct,
hourlyRate,
avgGrowthPct,
avgGrowthValue,
streaks,
lastDoubleDate,
lastDoubleDays,
lastDoubleValue,
doublingDays,
targetPrediction,
unitMode
};
};
const buildMetricCards = (metrics, unitMode = 'auto') => {
if (!metrics) return '';
const cards = metrics.map((metric) => `
<div class="ep-metric-card">
<h4>${metric.title}</h4>
<strong>${metric.value}</strong>
${metric.desc ? `<span>${metric.desc}</span>` : ''}
</div>
`).join('');
return `<div class="ep-metrics-grid">${cards}</div>`;
};
/* =========================
设置系统
========================= */
function getChartOptions() {
const defaults = {
viewMode: 'summary',
summaryShowCurrent: true,
summaryShowNonCurrent: true,
detailedShowEquipped: true,
detailedShowInventory: true,
detailedShowMarketListings: true,
detailedShowHouse: true,
detailedShowAbility: true,
showTotal: true,
daysToShow: 30,
showTimeRangeSettings: true,
visibleTimeRanges: [3, 7, 30, 60, 90, 180],
unitMode: 'auto',
showMetricsPanel: true
};
const saved = GM_getValue('chartOptions', defaults);
return {...defaults, ...saved};
}
function saveChartOptions(options) {
GM_setValue('chartOptions', options);
}
/* =========================
导入导出功能
========================= */
class ImportExportManager {
constructor(store) {
this.store = store;
this.importData = null;
}
// 导出数据
exportData(includeSettings = true) {
const exportObj = {
version: GM_info.script.version,
exportDate: new Date().toISOString(),
exportType: includeSettings ? 'full' : 'data_only',
data: {
assetData: this.store.data,
lastUpdate: readRoleLastUpdateMap()
}
};
// 如果包含设置,也导出设置
if (includeSettings) {
exportObj.settings = {
chartOptions: GM_getValue('chartOptions', {}),
metricsPrefs: readPrefs(STORAGE_KEYS.metricsPrefs),
metricsPanel: readPrefs(STORAGE_KEYS.metricsPanel)
};
}
return exportObj;
}
// 导出为JSON字符串
exportToJson(includeSettings = true) {
const exportObj = this.exportData(includeSettings);
return JSON.stringify(exportObj, null, 2);
}
// 导出并下载文件
downloadExport(includeSettings = true) {
const jsonStr = this.exportToJson(includeSettings);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const filename = `milkyway_assets_backup_${timestamp}.json`;
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showNotification(`${t('exportSuccess')} ${filename}`, 'success');
}
// 验证导入数据
validateImportData(data) {
if (!data || typeof data !== 'object') {
throw new Error(t('invalidFormat'));
}
if (!data.version) {
throw new Error(t('unrecognizedVersion'));
}
if (!data.data || !data.data.assetData) {
throw new Error(t('missingAssetData'));
}
return true;
}
// 预览导入数据
previewImport(jsonStr) {
try {
const data = JSON.parse(jsonStr);
this.validateImportData(data);
const assetData = data.data.assetData;
const roleCount = Object.keys(assetData).length;
const totalRecords = Object.values(assetData).reduce((sum, roleData) => {
return sum + (roleData ? Object.keys(roleData).length : 0);
}, 0);
let previewHtml = `
<div class="import-preview show">
<div class="import-preview-title">${t('preview')}</div>
<div class="import-preview-content">
<p>📄 ${t('version')}: ${data.version}</p>
<p>📅 ${t('exportTime')}: ${new Date(data.exportDate).toLocaleString()}</p>
<p>👥 ${t('roleCount')}: ${roleCount}</p>
<p>📊 ${t('totalRecords')}: ${totalRecords}</p>
<p>📋 ${t('containsSettings')}: ${data.settings ? t('yes') : t('no')}</p>
`;
// 显示每个角色的数据统计
if (roleCount > 0) {
previewHtml += `<p>📈 ${t('roleStats')}</p><ul>`;
Object.entries(assetData).forEach(([role, roleData]) => {
const recordCount = roleData ? Object.keys(roleData).length : 0;
previewHtml += `<li>${role}: ${recordCount}${t('records')}</li>`;
});
previewHtml += '</ul>';
}
previewHtml += `
</div>
<div class="import-preview-actions">
<button class="confirm-import-btn">${t('confirmImport')}</button>
<button class="cancel-import-btn">${t('cancel')}</button>
</div>
</div>
`;
this.importData = data;
return previewHtml;
} catch (error) {
throw new Error(`${t('previewFailed')}: ${error.message}`);
}
}
// 执行导入
executeImport(options = { merge: true }) {
if (!this.importData) {
throw new Error(t('noImportData'));
}
const data = this.importData;
// 导入资产数据
if (options.merge) {
// 合并模式:保留现有数据,添加新数据
Object.entries(data.data.assetData).forEach(([role, roleData]) => {
if (!this.store.data[role]) {
this.store.data[role] = {};
}
Object.assign(this.store.data[role], roleData);
});
} else {
// 覆盖模式:完全替换
this.store.data = data.data.assetData;
}
// 保存资产数据
this.store.saveToStorage();
// 导入最后更新时间
if (data.data.lastUpdate) {
const currentLastUpdate = readRoleLastUpdateMap();
if (options.merge) {
Object.assign(currentLastUpdate, data.data.lastUpdate);
writeRoleLastUpdateMap(currentLastUpdate);
} else {
writeRoleLastUpdateMap(data.data.lastUpdate);
}
}
// 导入设置
if (data.settings) {
if (data.settings.chartOptions) {
saveChartOptions(data.settings.chartOptions);
}
if (data.settings.metricsPrefs) {
writePrefs(STORAGE_KEYS.metricsPrefs, data.settings.metricsPrefs);
}
if (data.settings.metricsPanel) {
writePrefs(STORAGE_KEYS.metricsPanel, data.settings.metricsPanel);
}
}
this.importData = null;
// 刷新页面显示
location.reload();
}
// 显示通知
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
}
// 创建隐藏的文件输入元素
function createFileInput() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,application/json';
input.className = 'file-input-hidden';
input.id = 'importFileInput';
document.body.appendChild(input);
return input;
}
// 在设置弹窗中添加导入导出部分
function addImportExportToSettings(settingsModal, importExportManager) {
const settingsBody = document.getElementById('assetSettingsBody');
const importExportSection = document.createElement('div');
importExportSection.className = 'import-export-section';
importExportSection.innerHTML = `
<span class="settings-title">${t('importExport')}</span>
<div class="import-export-buttons">
<button class="import-export-btn export-all" id="exportFullBtn">${t('exportAll')}</button>
<button class="import-export-btn" id="exportDataBtn">${t('exportData')}</button>
<button class="import-export-btn import" id="importBtn">${t('importData')}</button>
</div>
<div id="importPreviewContainer"></div>
`;
settingsBody.appendChild(importExportSection);
// 创建文件输入
const fileInput = createFileInput();
// 导出全部(含设置)
document.getElementById('exportFullBtn').addEventListener('click', () => {
try {
importExportManager.downloadExport(true);
} catch (error) {
importExportManager.showNotification(`${t('exportFailed')}: ${error.message}`, 'error');
}
});
// 仅导出数据
document.getElementById('exportDataBtn').addEventListener('click', () => {
try {
importExportManager.downloadExport(false);
} catch (error) {
importExportManager.showNotification(`${t('exportFailed')}: ${error.message}`, 'error');
}
});
// 导入数据
document.getElementById('importBtn').addEventListener('click', () => {
fileInput.click();
});
// 处理文件选择
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const previewHtml = importExportManager.previewImport(e.target.result);
const previewContainer = document.getElementById('importPreviewContainer');
previewContainer.innerHTML = previewHtml;
// 绑定预览按钮事件
const confirmBtn = previewContainer.querySelector('.confirm-import-btn');
const cancelBtn = previewContainer.querySelector('.cancel-import-btn');
if (confirmBtn) {
confirmBtn.addEventListener('click', () => {
try {
importExportManager.executeImport({ merge: true });
} catch (error) {
importExportManager.showNotification(`${t('importFailed')}: ${error.message}`, 'error');
}
});
}
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
previewContainer.innerHTML = '';
importExportManager.importData = null;
fileInput.value = ''; // 清空文件输入
});
}
} catch (error) {
importExportManager.showNotification(error.message, 'error');
fileInput.value = ''; // 清空文件输入
}
};
reader.readAsText(file);
});
}
/* =========================
主逻辑
========================= */
window.kbd_calculateTotalNetworth = function kbd_calculateTotalNetworth(
equippedNetworth,
inventoryNetworth,
marketListingsNetworth,
totalHouseScore,
abilityScore,
dom
) {
// 检测角色ID
const detectRoleId = () => {
const candidates = [
document.querySelector('.CharacterName_name__1amXp span'),
document.querySelector('[class*="CharacterName_name"] span'),
document.querySelector('[data-testid="character-name"]'),
];
const text = (candidates.find(Boolean)?.textContent || '').replace(/\s+/g, ' ').trim();
return text || 'default';
};
const roleId = detectRoleId();
const store = new AssetDataStore();
store.setRole(roleId);
let chart = null;
let currentModal = null;
let metricsPanelVisible = getRoleBoolPref(STORAGE_KEYS.metricsPanel, roleId, true);
// 创建模态遮罩
function createOverlay() {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.id = 'modalOverlay';
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
hideAllModals();
}
});
document.body.appendChild(overlay);
return overlay;
}
// 显示遮罩
function showOverlay() {
const overlay = document.getElementById('modalOverlay') || createOverlay();
overlay.style.display = 'block';
}
// 隐藏遮罩
function hideOverlay() {
const overlay = document.getElementById('modalOverlay');
if (overlay) {
overlay.style.display = 'none';
}
}
// 隐藏所有弹窗
function hideAllModals() {
document.querySelectorAll('#deltaNetworthChartModal, #assetSettingsModal').forEach(modal => {
modal.style.display = 'none';
});
hideOverlay();
currentModal = null;
}
// 设置弹窗拖动
function setupDrag(modal) {
let isDragging = false;
let startX, startY, initialLeft, initialTop;
const header = modal.querySelector('#deltaNetworthChartHeader') || modal.querySelector('#assetSettingsHeader');
header.addEventListener('mousedown', (e) => {
if (e.target.className === 'close-btn') return;
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');
});
}
// 显示统计指标
function renderMetricsPanel() {
const entries = store.getHistoryEntriesSorted();
if (entries.length < 2) return '';
const options = getChartOptions();
const mtxAll = computeTotalMetricsFromEntries(entries, options.unitMode);
const lastUpdateIso = getRoleLastUpdate(roleId);
const lastUpdateStr = lastUpdateIso ? formatIsoToLocalDateTime(lastUpdateIso) : (mtxAll.latestRecordDate || '-');
const diffs = Array.isArray(mtxAll.differences) ? mtxAll.differences : [];
const last7Diffs = diffs.slice(-7);
const last30Diffs = diffs.slice(-30);
const winCount7 = last7Diffs.filter((d) => d && Number.isFinite(d.value) && d.value > 0).length;
const loseCount7 = last7Diffs.filter((d) => d && Number.isFinite(d.value) && d.value < 0).length;
const flatCount7 = last7Diffs.filter((d) => d && Number.isFinite(d.value) && d.value === 0).length;
const rate7 = last7Diffs.length ? (winCount7 / last7Diffs.length) * 100 : null;
const avg30Value = last30Diffs.length ? last30Diffs.reduce((sum, d) => sum + (d.value || 0), 0) / last30Diffs.length : null;
const avg30Pct = last30Diffs.length ? last30Diffs.reduce((sum, d) => sum + (d.growthPct || 0), 0) / last30Diffs.length : null;
const bestWorstValue = `${formatSignedLargeNumber(mtxAll.streaks?.bestGain || 0, options.unitMode)} / ${formatSignedLargeNumber(mtxAll.streaks?.worstLoss || 0, options.unitMode)}`;
const bestWorstDesc = `${mtxAll.streaks?.bestDay || '-'} | ${mtxAll.streaks?.worstDay || '-'}`;
const predictBits = [];
if (mtxAll.doublingDays) predictBits.push(`${t('doubling')}: ${mtxAll.doublingDays}${t('days')}`);
if (mtxAll.targetPrediction) predictBits.push(`${t('target')}: ${mtxAll.targetPrediction.days}${t('days')}→${formatLargeNumber(mtxAll.targetPrediction.targetValue, options.unitMode)}`);
const metrics = [
{ title: t('currentAssets'), value: formatLargeNumber(mtxAll.valueToPersist || 0, options.unitMode), desc: `${t('recentUpdate')}: ${lastUpdateStr}` },
{
title: t('dailyChange'),
value: `${(mtxAll.growthPct >= 0 ? '+' : '')}${(mtxAll.growthPct || 0).toFixed(2)}%`,
desc: formatSignedLargeNumber(mtxAll.todayDelta?.value || 0, options.unitMode),
},
{ title: t('hourlyRate'), value: formatLargeNumber(mtxAll.hourlyRate || 0, options.unitMode), desc: t('hourlyRateDesc') },
{
title: t('avg7Growth'),
value: `${(mtxAll.avgGrowthPct >= 0 ? '+' : '')}${(mtxAll.avgGrowthPct || 0).toFixed(2)}% / ${formatSignedLargeNumber(mtxAll.avgGrowthValue || 0, options.unitMode)}`,
desc: t('avg7Desc'),
},
{
title: t('winRate7'),
value: rate7 === null ? '—' : `${winCount7}/${last7Diffs.length} (${rate7.toFixed(0)}%)`,
desc: `${t('profitLossFlat')}: ${winCount7}/${loseCount7}/${flatCount7}`,
},
{
title: t('avg30Daily'),
value: avg30Value === null ? '—' : `${(avg30Pct >= 0 ? '+' : '')}${(avg30Pct || 0).toFixed(2)}% / ${formatSignedLargeNumber(avg30Value || 0, options.unitMode)}`,
desc: predictBits.length ? `${predictBits.join(' | ')}` : t('avg7Desc'),
},
{ title: t('bestWorstDay'), value: bestWorstValue, desc: bestWorstDesc },
{
title: t('lastDouble'),
value: Number.isFinite(mtxAll.lastDoubleDays) ? `${mtxAll.lastDoubleDays} ${t('days')}` : t('noRecord'),
desc: mtxAll.lastDoubleDate ? `${mtxAll.lastDoubleDate}: ${formatLargeNumber(mtxAll.lastDoubleValue || 0, options.unitMode)}` : '—',
},
];
return buildMetricCards(metrics);
}
// 更新显示
const updateDisplay = (isFirst = false) => {
store.setTodayDetailedValues(
equippedNetworth,
inventoryNetworth,
marketListingsNetworth,
totalHouseScore,
abilityScore
);
setRoleLastUpdate(roleId);
const deltas = store.getTodayDeltas();
const options = getChartOptions();
const formattedTotalDelta = formatLargeNumber(deltas.totalDelta, options.unitMode);
const totalDeltaClass = deltas.totalDelta > 0 ? 'positive-delta' :
(deltas.totalDelta < 0 ? 'negative-delta' : 'neutral-delta');
// 计算近7天日均
const entries = store.getHistoryEntriesSorted();
const last7 = entries.slice(-8);
let avg7 = 0;
if (last7.length >= 2) {
let s = 0;
let c = 0;
for (let i = 1; i < last7.length; i++) {
const d = last7[i][1] - last7[i - 1][1];
if (Number.isFinite(d)) { s += d; c += 1; }
}
avg7 = c ? s / c : 0;
}
const lastUpdateIso = getRoleLastUpdate(roleId);
const lastUpdateStr = lastUpdateIso ? formatIsoToLocalDateTime(lastUpdateIso) : (entries.length ? entries[entries.length - 1][0] : '—');
if (isFirst) {
const metricsHTML = metricsPanelVisible ? renderMetricsPanel() : '';
dom.insertAdjacentHTML('afterend', `
<div id="assetDeltaContainer" style="margin-top: 0px;">
<div class="asset-delta-display">
<span class="asset-delta-label">${t('assetsGrowth')}:</span>
<span class="${totalDeltaClass}">${formattedTotalDelta}</span>
<span id="showHistoryIcon" title="${t('historyIcon')}">📊</span>
<span id="showMetricsIcon" title="${metricsPanelVisible ? t('metricsIconHide') : t('metricsIconShow')}">📈</span>
<span id="languageToggle" title="中文/English">🌐</span>
<span id="settingsToggle" title="${t('settingsIcon')}">⚙️</span>
</div>
<div class="ep-delta-extra">
<span>${t('avg7Days')}: ${formatSignedLargeNumber(avg7, options.unitMode)}</span>
<span>${t('lastRecord')}: ${lastUpdateStr}</span>
</div>
${metricsHTML}
</div>
`);
// 创建图表弹窗
const chartModal = document.createElement('div');
chartModal.id = 'deltaNetworthChartModal';
chartModal.innerHTML = `
<div id="deltaNetworthChartHeader">
<span>${t('chartTitle')} (v${GM_info.script.version})</span>
<span class="close-btn" id="chartModalCloseBtn">❌</span>
</div>
<div id="chartOptionsContainer">
<div id="timeRangeOptions">
<span style="margin-right:10px;font-weight:bold;color:#fff;">${t('timeRange')}</span>
</div>
</div>
<div id="ep-metrics-container"></div>
<div id="netWorthChartBody">
<canvas id="netWorthChart"></canvas>
</div>
`;
document.body.appendChild(chartModal);
// 创建设置弹窗
const settingsModal = document.createElement('div');
settingsModal.id = 'assetSettingsModal';
settingsModal.innerHTML = `
<div id="assetSettingsHeader">
<span>${t('settingsTitle')} (v${GM_info.script.version})</span>
<span class="close-btn" id="settingsModalCloseBtn">❌</span>
</div>
<div id="assetSettingsBody">
<!-- 视图切换 -->
<div class="settings-section">
<span class="settings-title">${t('viewMode')}</span>
<div class="view-toggle">
<div class="view-option active" data-view="summary">${t('summaryView')}</div>
<div class="view-option" data-view="detailed">${t('detailedView')}</div>
</div>
</div>
<!-- 摘要视图选项 -->
<div class="settings-section summary-view-options">
<span class="settings-title">${t('summaryOptions')}</span>
<div class="chart-option">
<input type="checkbox" id="summaryShowCurrent">
<label for="summaryShowCurrent">${t('showCurrent')}</label>
</div>
<div class="chart-option">
<input type="checkbox" id="summaryShowNonCurrent">
<label for="summaryShowNonCurrent">${t('showNonCurrent')}</label>
</div>
</div>
<!-- 详细视图选项 -->
<div class="settings-section detailed-view-options hidden">
<span class="settings-title">${t('detailedOptions')}</span>
<div class="chart-option">
<input type="checkbox" id="detailedShowEquipped">
<label for="detailedShowEquipped">${t('showEquipped')}</label>
</div>
<div class="chart-option">
<input type="checkbox" id="detailedShowInventory">
<label for="detailedShowInventory">${t('showInventory')}</label>
</div>
<div class="chart-option">
<input type="checkbox" id="detailedShowMarketListings">
<label for="detailedShowMarketListings">${t('showMarket')}</label>
</div>
<div class="chart-option">
<input type="checkbox" id="detailedShowHouse">
<label for="detailedShowHouse">${t('showHouse')}</label>
</div>
<div class="chart-option">
<input type="checkbox" id="detailedShowAbility">
<label for="detailedShowAbility">${t('showAbility')}</label>
</div>
</div>
<!-- 通用选项 -->
<div class="settings-section">
<span class="settings-title">${t('generalOptions')}</span>
<div class="chart-option">
<input type="checkbox" id="showTotalOption">
<label for="showTotalOption">${t('showTotal')}</label>
</div>
</div>
<!-- 单位设置 -->
<div class="settings-section">
<span class="settings-title">${t('unitSettings')}</span>
<div class="settings-group">
<span class="settings-label">${t('unitMode')}</span>
<div class="unit-toggle">
<div class="unit-option active" data-unit="auto">${t('unitAuto')}</div>
<div class="unit-option" data-unit="k">${t('unitK')}</div>
<div class="unit-option" data-unit="m">${t('unitM')}</div>
<div class="unit-option" data-unit="b">${t('unitB')}</div>
</div>
</div>
</div>
<!-- 时间范围设置 -->
<div class="settings-section">
<span class="settings-title">${t('timeRangeSettings')}</span>
<div class="settings-group">
<label class="settings-label">
<input type="checkbox" id="showTimeRangeToggle">
${t('showTimeRange')}
</label>
</div>
<div class="settings-group">
<span class="settings-label">${t('availableTimeRanges')}</span>
<div style="margin-top: 5px;">
<label style="color:#fff; margin-right:15px; display:inline-block;">
<input type="checkbox" id="timeRange3"> 3${t('days')}
</label>
<label style="color:#fff; margin-right:15px; display:inline-block;">
<input type="checkbox" id="timeRange7"> 7${t('days')}
</label>
<label style="color:#fff; margin-right:15px; display:inline-block;">
<input type="checkbox" id="timeRange30"> 30${t('days')}
</label>
<label style="color:#fff; margin-right:15px; display:inline-block;">
<input type="checkbox" id="timeRange60"> 60${t('days')}
</label>
<label style="color:#fff; margin-right:15px; display:inline-block;">
<input type="checkbox" id="timeRange90"> 90${t('days')}
</label>
<label style="color:#fff; display:inline-block;">
<input type="checkbox" id="timeRange180"> 180${t('days')}
</label>
</div>
</div>
<!-- 添加时间范围按钮容器 -->
<div class="settings-group">
<span class="settings-label">${t('currentButtons')}</span>
<div id="settingsTimeRangeButtons" class="time-range-buttons" style="margin-top: 10px;">
<!-- 时间范围按钮会动态添加到这里 -->
</div>
</div>
</div>
</div>
`;
document.body.appendChild(settingsModal);
// 初始化设置
initSettings();
// 创建导入导出管理器
const importExportManager = new ImportExportManager(store);
// 在设置弹窗中添加导入导出功能
addImportExportToSettings(settingsModal, importExportManager);
// 事件监听
document.getElementById('showHistoryIcon').addEventListener('click', () => showChartModal());
document.getElementById('settingsToggle').addEventListener('click', () => showSettingsModal());
document.getElementById('showMetricsIcon').addEventListener('click', toggleMetricsPanel);
document.getElementById('languageToggle').addEventListener('click', toggleLanguage);
// 关闭按钮事件
document.getElementById('chartModalCloseBtn').addEventListener('click', hideAllModals);
document.getElementById('settingsModalCloseBtn').addEventListener('click', hideAllModals);
// 视图切换事件
document.querySelectorAll('.view-option').forEach(option => {
option.addEventListener('click', (e) => {
const view = e.target.dataset.view;
switchView(view);
});
});
// 单位切换事件
document.querySelectorAll('.unit-option').forEach(option => {
option.addEventListener('click', (e) => {
const unit = e.target.dataset.unit;
switchUnit(unit);
});
});
// 选项变化监听
document.getElementById('summaryShowCurrent').addEventListener('change', updateChartVisibility);
document.getElementById('summaryShowNonCurrent').addEventListener('change', updateChartVisibility);
document.getElementById('detailedShowEquipped').addEventListener('change', updateChartVisibility);
document.getElementById('detailedShowInventory').addEventListener('change', updateChartVisibility);
document.getElementById('detailedShowMarketListings').addEventListener('change', updateChartVisibility);
document.getElementById('detailedShowHouse').addEventListener('change', updateChartVisibility);
document.getElementById('detailedShowAbility').addEventListener('change', updateChartVisibility);
document.getElementById('showTotalOption').addEventListener('change', updateChartVisibility);
// 时间范围设置监听
document.getElementById('showTimeRangeToggle').addEventListener('change', toggleTimeRangeVisibility);
document.getElementById('timeRange3').addEventListener('change', updateTimeRangeSettings);
document.getElementById('timeRange7').addEventListener('change', updateTimeRangeSettings);
document.getElementById('timeRange30').addEventListener('change', updateTimeRangeSettings);
document.getElementById('timeRange60').addEventListener('change', updateTimeRangeSettings);
document.getElementById('timeRange90').addEventListener('change', updateTimeRangeSettings);
document.getElementById('timeRange180').addEventListener('change', updateTimeRangeSettings);
// 初始化时间范围按钮
updateSettingsTimeRangeButtons();
// 设置拖动
setupDrag(chartModal);
setupDrag(settingsModal);
} else {
const container = document.getElementById('assetDeltaContainer');
if (container) {
const metricsHTML = metricsPanelVisible ? renderMetricsPanel() : '';
container.innerHTML = `
<div class="asset-delta-display">
<span class="asset-delta-label">${t('assetsGrowth')}:</span>
<span class="${totalDeltaClass}">${formattedTotalDelta}</span>
<span id="showHistoryIcon" title="${t('historyIcon')}">📊</span>
<span id="showMetricsIcon" title="${metricsPanelVisible ? t('metricsIconHide') : t('metricsIconShow')}">📈</span>
<span id="languageToggle" title="中文/English">🌐</span>
<span id="settingsToggle" title="${t('settingsIcon')}">⚙️</span>
</div>
<div class="ep-delta-extra">
<span>${t('avg7Days')}: ${formatSignedLargeNumber(avg7, options.unitMode)}</span>
<span>${t('lastRecord')}: ${lastUpdateStr}</span>
</div>
${metricsHTML}
`;
// 重新绑定事件
document.getElementById('showHistoryIcon').addEventListener('click', () => showChartModal());
document.getElementById('settingsToggle').addEventListener('click', () => showSettingsModal());
document.getElementById('showMetricsIcon').addEventListener('click', toggleMetricsPanel);
document.getElementById('languageToggle').addEventListener('click', toggleLanguage);
}
}
};
// 切换统计指标面板
function toggleMetricsPanel() {
metricsPanelVisible = !metricsPanelVisible;
setRoleBoolPref(STORAGE_KEYS.metricsPanel, roleId, metricsPanelVisible);
updateDisplay();
}
// 切换视图模式
function switchView(viewMode) {
const options = getChartOptions();
options.viewMode = viewMode;
saveChartOptions(options);
document.querySelectorAll('.view-option').forEach(option => {
if (option.dataset.view === viewMode) {
option.classList.add('active');
} else {
option.classList.remove('active');
}
});
const summaryOptions = document.querySelector('.summary-view-options');
const detailedOptions = document.querySelector('.detailed-view-options');
if (viewMode === 'summary') {
summaryOptions.classList.remove('hidden');
detailedOptions.classList.add('hidden');
} else {
summaryOptions.classList.add('hidden');
detailedOptions.classList.remove('hidden');
}
if (chart) {
recreateChart();
}
}
// 切换单位模式
function switchUnit(unitMode) {
const options = getChartOptions();
options.unitMode = unitMode;
saveChartOptions(options);
document.querySelectorAll('.unit-option').forEach(option => {
if (option.dataset.unit === unitMode) {
option.classList.add('active');
} else {
option.classList.remove('active');
}
});
// 更新显示和图表
updateDisplay();
if (chart) {
chart.destroy();
chart = null;
initializeChart();
}
}
// 初始化设置
function initSettings() {
const options = getChartOptions();
// 设置视图模式
switchView(options.viewMode);
// 设置摘要视图选项
document.getElementById('summaryShowCurrent').checked = options.summaryShowCurrent;
document.getElementById('summaryShowNonCurrent').checked = options.summaryShowNonCurrent;
// 设置详细视图选项
document.getElementById('detailedShowEquipped').checked = options.detailedShowEquipped;
document.getElementById('detailedShowInventory').checked = options.detailedShowInventory;
document.getElementById('detailedShowMarketListings').checked = options.detailedShowMarketListings;
document.getElementById('detailedShowHouse').checked = options.detailedShowHouse;
document.getElementById('detailedShowAbility').checked = options.detailedShowAbility;
// 设置通用选项
document.getElementById('showTotalOption').checked = options.showTotal;
// 设置单位选项
switchUnit(options.unitMode);
// 设置时间范围显示选项
document.getElementById('showTimeRangeToggle').checked = options.showTimeRangeSettings;
// 设置时间范围复选框
document.getElementById('timeRange3').checked = options.visibleTimeRanges.includes(3);
document.getElementById('timeRange7').checked = options.visibleTimeRanges.includes(7);
document.getElementById('timeRange30').checked = options.visibleTimeRanges.includes(30);
document.getElementById('timeRange60').checked = options.visibleTimeRanges.includes(60);
document.getElementById('timeRange90').checked = options.visibleTimeRanges.includes(90);
document.getElementById('timeRange180').checked = options.visibleTimeRanges.includes(180);
// 控制时间范围选项的显示
const timeRangeOptions = document.getElementById('timeRangeOptions');
if (timeRangeOptions) {
if (options.showTimeRangeSettings) {
timeRangeOptions.classList.remove('hidden');
} else {
timeRangeOptions.classList.add('hidden');
}
}
// 更新设置弹窗中的时间范围按钮
updateSettingsTimeRangeButtons();
}
// 重新创建图表
function recreateChart() {
if (chart) {
chart.destroy();
chart = null;
}
initializeChart();
}
// 显示图表弹窗
function showChartModal() {
hideAllModals();
currentModal = document.getElementById('deltaNetworthChartModal');
showOverlay();
currentModal.style.display = 'flex';
// 显示统计指标
const metricsContainer = document.getElementById('ep-metrics-container');
if (metricsContainer) {
metricsContainer.innerHTML = renderMetricsPanel();
}
if (!window.Chart) {
loadChartLibrary().then(() => {
initializeChart();
updateChartTimeRangeButtons();
});
} else if (!chart) {
initializeChart();
updateChartTimeRangeButtons();
} else {
updateChart();
updateChartTimeRangeButtons();
}
}
// 显示设置弹窗
function showSettingsModal() {
hideAllModals();
currentModal = document.getElementById('assetSettingsModal');
showOverlay();
currentModal.style.display = 'flex';
// 更新时间范围按钮
updateSettingsTimeRangeButtons();
}
function loadChartLibrary() {
return new Promise((resolve) => {
if (window.Chart) {
resolve();
return;
}
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');
const datasets = [];
if (options.viewMode === 'detailed') {
datasets.push({
id: 'equipped',
label: t('equipmentValue'),
data: historyData.equippedNetworth,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
tension: 0.3,
fill: false,
hidden: !options.detailedShowEquipped
});
datasets.push({
id: 'inventory',
label: t('inventoryValue'),
data: historyData.inventoryNetworth,
borderColor: 'rgba(255, 159, 64, 1)',
backgroundColor: 'rgba(255, 159, 64, 0.1)',
tension: 0.3,
fill: false,
hidden: !options.detailedShowInventory
});
datasets.push({
id: 'market',
label: t('marketValue'),
data: historyData.marketListingsNetworth,
borderColor: 'rgba(153, 102, 255, 1)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
tension: 0.3,
fill: false,
hidden: !options.detailedShowMarketListings
});
datasets.push({
id: 'house',
label: t('houseValue'),
data: historyData.totalHouseScore,
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
tension: 0.3,
fill: false,
hidden: !options.detailedShowHouse
});
datasets.push({
id: 'ability',
label: t('abilityValue'),
data: historyData.abilityScore,
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
tension: 0.3,
fill: false,
hidden: !options.detailedShowAbility
});
} else {
datasets.push({
id: 'current',
label: t('currentAssetsValue'),
data: historyData.currentAssets,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
tension: 0.3,
fill: false,
hidden: !options.summaryShowCurrent
});
datasets.push({
id: 'nonCurrent',
label: t('nonCurrentAssetsValue'),
data: historyData.nonCurrentAssets,
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
tension: 0.3,
fill: false,
hidden: !options.summaryShowNonCurrent
});
}
datasets.push({
id: 'total',
label: t('totalAssets'),
data: historyData.totalAssets,
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
tension: 0.3,
fill: false,
hidden: !options.showTotal,
borderWidth: 2
});
chart = new Chart(ctx, {
type: 'line',
data: {
labels: historyData.labels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top',
labels: {
usePointStyle: true,
boxWidth: 10,
color: '#fff'
}
},
tooltip: {
callbacks: {
label: (context) => {
const label = context.dataset.label || '';
const value = formatLargeNumber(context.raw, options.unitMode);
return `${label}: ${value}`;
}
},
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: '#fff',
bodyColor: '#fff'
}
},
scales: {
x: {
ticks: {
color: '#ccc'
},
grid: {
color: 'rgba(255, 255, 255, 0.1)'
}
},
y: {
ticks: {
color: '#ccc',
callback: (value) => formatLargeNumber(value, options.unitMode)
},
grid: {
color: 'rgba(255, 255, 255, 0.1)'
}
}
}
}
});
}
function updateChart() {
const options = getChartOptions();
const historyData = store.getHistoryData(options.daysToShow);
chart.data.labels = historyData.labels;
if (options.viewMode === 'detailed') {
if (chart.data.datasets.length >= 6) {
chart.data.datasets[0].data = historyData.equippedNetworth;
chart.data.datasets[1].data = historyData.inventoryNetworth;
chart.data.datasets[2].data = historyData.marketListingsNetworth;
chart.data.datasets[3].data = historyData.totalHouseScore;
chart.data.datasets[4].data = historyData.abilityScore;
}
} else {
if (chart.data.datasets.length >= 3) {
chart.data.datasets[0].data = historyData.currentAssets;
chart.data.datasets[1].data = historyData.nonCurrentAssets;
}
}
const totalIndex = options.viewMode === 'detailed' ? 5 : 2;
if (chart.data.datasets[totalIndex]) {
chart.data.datasets[totalIndex].data = historyData.totalAssets;
}
// 更新图表配置以反映新的单位模式
chart.options.scales.y.ticks.callback = (value) => formatLargeNumber(value, options.unitMode);
chart.options.plugins.tooltip.callbacks.label = (context) => {
const label = context.dataset.label || '';
const value = formatLargeNumber(context.raw, options.unitMode);
return `${label}: ${value}`;
};
chart.update();
}
function updateChartVisibility() {
const options = getChartOptions();
options.summaryShowCurrent = document.getElementById('summaryShowCurrent').checked;
options.summaryShowNonCurrent = document.getElementById('summaryShowNonCurrent').checked;
options.detailedShowEquipped = document.getElementById('detailedShowEquipped').checked;
options.detailedShowInventory = document.getElementById('detailedShowInventory').checked;
options.detailedShowMarketListings = document.getElementById('detailedShowMarketListings').checked;
options.detailedShowHouse = document.getElementById('detailedShowHouse').checked;
options.detailedShowAbility = document.getElementById('detailedShowAbility').checked;
options.showTotal = document.getElementById('showTotalOption').checked;
saveChartOptions(options);
if (chart) {
const historyData = store.getHistoryData(options.daysToShow);
if (options.viewMode === 'detailed') {
if (chart.data.datasets.length >= 6) {
chart.data.datasets[0].hidden = !options.detailedShowEquipped;
chart.data.datasets[1].hidden = !options.detailedShowInventory;
chart.data.datasets[2].hidden = !options.detailedShowMarketListings;
chart.data.datasets[3].hidden = !options.detailedShowHouse;
chart.data.datasets[4].hidden = !options.detailedShowAbility;
chart.data.datasets[5].hidden = !options.showTotal;
}
} else {
if (chart.data.datasets.length >= 3) {
chart.data.datasets[0].hidden = !options.summaryShowCurrent;
chart.data.datasets[1].hidden = !options.summaryShowNonCurrent;
chart.data.datasets[2].hidden = !options.showTotal;
}
}
chart.update();
}
}
function toggleTimeRangeVisibility() {
const show = document.getElementById('showTimeRangeToggle').checked;
const timeRangeOptions = document.getElementById('timeRangeOptions');
const options = getChartOptions();
options.showTimeRangeSettings = show;
saveChartOptions(options);
if (show) {
timeRangeOptions.classList.remove('hidden');
} else {
timeRangeOptions.classList.add('hidden');
}
}
function updateTimeRangeSettings() {
const options = getChartOptions();
const visibleRanges = [];
if (document.getElementById('timeRange3').checked) visibleRanges.push(3);
if (document.getElementById('timeRange7').checked) visibleRanges.push(7);
if (document.getElementById('timeRange30').checked) visibleRanges.push(30);
if (document.getElementById('timeRange60').checked) visibleRanges.push(60);
if (document.getElementById('timeRange90').checked) visibleRanges.push(90);
if (document.getElementById('timeRange180').checked) visibleRanges.push(180);
options.visibleTimeRanges = visibleRanges;
saveChartOptions(options);
updateChartTimeRangeButtons();
updateSettingsTimeRangeButtons();
}
function updateChartTimeRangeButtons() {
const timeRangeOptions = document.getElementById('timeRangeOptions');
const options = getChartOptions();
const titleSpan = timeRangeOptions.querySelector('span');
timeRangeOptions.innerHTML = '';
if (titleSpan) {
timeRangeOptions.appendChild(titleSpan);
}
options.visibleTimeRanges.forEach(days => {
const btn = document.createElement('button');
btn.id = `btn${days}Days`;
btn.className = 'time-range-btn';
if (options.daysToShow === days) {
btn.classList.add('active');
}
btn.textContent = `${days}${t('days')}`;
btn.addEventListener('click', () => updateChartTimeRange(days));
timeRangeOptions.appendChild(btn);
});
}
function updateSettingsTimeRangeButtons() {
const buttonsContainer = document.getElementById('settingsTimeRangeButtons');
const options = getChartOptions();
if (!buttonsContainer) {
console.error('settingsTimeRangeButtons container not found');
return;
}
buttonsContainer.innerHTML = '';
options.visibleTimeRanges.forEach(days => {
const btn = document.createElement('button');
btn.id = `settingsBtn${days}Days`;
btn.className = 'time-range-btn';
if (options.daysToShow === days) {
btn.classList.add('active');
}
btn.textContent = `${days}${t('days')}`;
btn.addEventListener('click', () => {
updateChartTimeRange(days);
updateSettingsTimeRangeButtons();
updateChartTimeRangeButtons(); // 同时更新图表弹窗的按钮
});
buttonsContainer.appendChild(btn);
});
}
function updateChartTimeRange(days) {
const options = getChartOptions();
options.daysToShow = days;
saveChartOptions(options);
updateChartTimeRangeButtons();
updateSettingsTimeRangeButtons(); // 同时更新设置弹窗中的按钮
if (chart) {
const historyData = store.getHistoryData(days);
chart.data.labels = historyData.labels;
if (options.viewMode === 'detailed') {
if (chart.data.datasets.length >= 6) {
chart.data.datasets[0].data = historyData.equippedNetworth;
chart.data.datasets[1].data = historyData.inventoryNetworth;
chart.data.datasets[2].data = historyData.marketListingsNetworth;
chart.data.datasets[3].data = historyData.totalHouseScore;
chart.data.datasets[4].data = historyData.abilityScore;
}
} else {
if (chart.data.datasets.length >= 3) {
chart.data.datasets[0].data = historyData.currentAssets;
chart.data.datasets[1].data = historyData.nonCurrentAssets;
}
}
const totalIndex = options.viewMode === 'detailed' ? 5 : 2;
if (chart.data.datasets[totalIndex]) {
chart.data.datasets[totalIndex].data = historyData.totalAssets;
}
chart.update();
}
}
// 初始更新
updateDisplay(true);
setInterval(() => updateDisplay(false), 10 * 60 * 1000);
};
/* =========================
页面检测与执行
========================= */
const checkAssetsAndRun = async () => {
console.log('[DailyAssets] 开始检查资产数据...');
// 等待MWITools加载并显示数据
const mwValues = await getMWIToolsValues();
console.log('[DailyAssets] 获取到的资产价值:', mwValues);
const insertDom = document.getElementById('netWorthDetails');
if (insertDom && !document.getElementById('assetDeltaContainer')) {
window.kbd_calculateTotalNetworth?.(
mwValues.equippedNetworth,
mwValues.inventoryNetworth,
mwValues.marketListingsNetworth,
mwValues.totalHouseScore,
mwValues.abilityScore,
insertDom
);
}
};
// 初始检查和定时检查
console.log('[DailyAssets] 脚本加载完成,等待页面初始化...');
console.log('[DailyAssets] 当前语言:', GM_getValue('dailyAssetsLanguage', 'zh'));
// 等待页面完全加载
window.addEventListener('load', () => {
setTimeout(checkAssetsAndRun, 5000); // 等待5秒让MWITools加载
});
// 如果页面已经加载完成,立即执行
if (document.readyState === 'complete') {
setTimeout(checkAssetsAndRun, 5000);
}
// 每60秒检查一次数据更新
setInterval(checkAssetsAndRun, 60000);
})();